From 9d02a0583023cc3bfb167bf3011d642acd948a27 Mon Sep 17 00:00:00 2001 From: mck Date: Thu, 7 Dec 2023 23:36:54 +0100 Subject: [PATCH 01/24] [maven-release-plugin] prepare for next development iteration --- bom/pom.xml | 18 ++--- core-shaded/pom.xml | 2 +- core/pom.xml | 2 +- distribution-source/pom.xml | 125 +++++++++++++++++++++++++++++++++++ distribution-tests/pom.xml | 122 ++++++++++++++++++++++++++++++++++ distribution/pom.xml | 2 +- examples/pom.xml | 2 +- integration-tests/pom.xml | 2 +- mapper-processor/pom.xml | 2 +- mapper-runtime/pom.xml | 2 +- metrics/micrometer/pom.xml | 2 +- metrics/microprofile/pom.xml | 2 +- osgi-tests/pom.xml | 2 +- pom.xml | 2 +- query-builder/pom.xml | 2 +- test-infra/pom.xml | 2 +- 16 files changed, 269 insertions(+), 22 deletions(-) create mode 100644 distribution-source/pom.xml create mode 100644 distribution-tests/pom.xml diff --git a/bom/pom.xml b/bom/pom.xml index 4f93166237e..0fbb66dcc4e 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-bom pom @@ -38,42 +38,42 @@ com.scylladb java-driver-core - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-core-shaded - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-mapper-processor - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-mapper-runtime - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-query-builder - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-test-infra - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-metrics-micrometer - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.scylladb java-driver-metrics-microprofile - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT com.datastax.oss diff --git a/core-shaded/pom.xml b/core-shaded/pom.xml index 389c2144f78..c8f707175ee 100644 --- a/core-shaded/pom.xml +++ b/core-shaded/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-core-shaded Java driver for Scylla and Apache Cassandra(R) - core with shaded deps diff --git a/core/pom.xml b/core/pom.xml index 0f6d8647e22..fd86f4431dc 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-core bundle diff --git a/distribution-source/pom.xml b/distribution-source/pom.xml new file mode 100644 index 00000000000..29ac63e98a5 --- /dev/null +++ b/distribution-source/pom.xml @@ -0,0 +1,125 @@ + + + + 4.0.0 + + org.apache.cassandra + java-driver-parent + 4.18.1.0-SNAPSHOT + + java-driver-distribution-source + pom + Apache Cassandra Java Driver - source distribution + + apache-cassandra-java-driver-${project.version}-source + + + maven-jar-plugin + + + + default-jar + none + + + + + maven-source-plugin + + true + + + + maven-install-plugin + + true + + + + maven-deploy-plugin + + true + + + + org.revapi + revapi-maven-plugin + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + true + + + + + + + release + + + + maven-assembly-plugin + + + assemble-source-tarball + package + + single + + + + + false + + src/assembly/source-tarball.xml + + posix + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + 1.7 + + + + artifacts + + + + + true + + sha256 + sha512 + + + + + + + + diff --git a/distribution-tests/pom.xml b/distribution-tests/pom.xml new file mode 100644 index 00000000000..504ef24ebdb --- /dev/null +++ b/distribution-tests/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + org.apache.cassandra + java-driver-parent + 4.18.1.0-SNAPSHOT + + java-driver-distribution-tests + Apache Cassandra Java Driver - distribution tests + + + + ${project.groupId} + java-driver-bom + ${project.version} + pom + import + + + + + + org.apache.cassandra + java-driver-test-infra + test + + + org.apache.cassandra + java-driver-query-builder + test + + + org.apache.cassandra + java-driver-mapper-processor + test + + + org.apache.cassandra + java-driver-mapper-runtime + test + + + org.apache.cassandra + java-driver-core + test + + + org.apache.cassandra + java-driver-metrics-micrometer + test + + + org.apache.cassandra + java-driver-metrics-microprofile + test + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${testing.jvm}/bin/java + ${mockitoopens.argline} + 1 + + + + org.revapi + revapi-maven-plugin + + true + + + + maven-install-plugin + + true + + + + maven-deploy-plugin + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + true + + + + + diff --git a/distribution/pom.xml b/distribution/pom.xml index 502b7ecb1ff..06f54854ef0 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-distribution diff --git a/examples/pom.xml b/examples/pom.xml index 391d334fc89..5f55d3e04cf 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -28,7 +28,7 @@ java-driver-parent com.scylladb - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-examples Java driver for Scylla and Apache Cassandra(R) - examples. diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 60631228e77..627bb8f66f2 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-integration-tests jar diff --git a/mapper-processor/pom.xml b/mapper-processor/pom.xml index 7ef41b00963..96fcb636d44 100644 --- a/mapper-processor/pom.xml +++ b/mapper-processor/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-mapper-processor Java driver for Scylla and Apache Cassandra(R) - object mapper processor diff --git a/mapper-runtime/pom.xml b/mapper-runtime/pom.xml index 42b7a968c7f..13f25db2230 100644 --- a/mapper-runtime/pom.xml +++ b/mapper-runtime/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-mapper-runtime bundle diff --git a/metrics/micrometer/pom.xml b/metrics/micrometer/pom.xml index d96c7cb4720..6d0110e9b55 100644 --- a/metrics/micrometer/pom.xml +++ b/metrics/micrometer/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT ../../ java-driver-metrics-micrometer diff --git a/metrics/microprofile/pom.xml b/metrics/microprofile/pom.xml index 7771e0ad6e5..b9a07939a73 100644 --- a/metrics/microprofile/pom.xml +++ b/metrics/microprofile/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT ../../ java-driver-metrics-microprofile diff --git a/osgi-tests/pom.xml b/osgi-tests/pom.xml index 96d4784b99c..37463fb547f 100644 --- a/osgi-tests/pom.xml +++ b/osgi-tests/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-osgi-tests jar diff --git a/pom.xml b/pom.xml index 414ec6b5eb2..0ec1d75c8c7 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 4.0.0 com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT pom Java Driver for Scylla and Apache Cassandra A driver for Scylla and Apache Cassandra(R) 2.1+ that works exclusively with the Cassandra Query Language version 3 (CQL3) and Cassandra's native protocol versions 3 and above. diff --git a/query-builder/pom.xml b/query-builder/pom.xml index aa79ba1118e..d718fce2d23 100644 --- a/query-builder/pom.xml +++ b/query-builder/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-query-builder bundle diff --git a/test-infra/pom.xml b/test-infra/pom.xml index aa3c07445bb..8c775ce4a93 100644 --- a/test-infra/pom.xml +++ b/test-infra/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.0.2-SNAPSHOT + 4.18.1.0-SNAPSHOT java-driver-test-infra bundle From a20d70387417f55739f0b3374f46f790451430f0 Mon Sep 17 00:00:00 2001 From: mck Date: Fri, 8 Dec 2023 00:16:34 +0100 Subject: [PATCH 02/24] Remove distributionManagement, the apache parent defines this for us --- pom.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pom.xml b/pom.xml index 0ec1d75c8c7..3fde3ac8233 100644 --- a/pom.xml +++ b/pom.xml @@ -1020,16 +1020,6 @@ height="0" width="0" style="display:none;visibility:hidden"> - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - Apache 2 From 12b559c22453fed0cd7b29341969fc39125b24a9 Mon Sep 17 00:00:00 2001 From: mck Date: Fri, 8 Dec 2023 10:08:03 +0100 Subject: [PATCH 03/24] Remove fossa dependency analysis github action ASF does not have a subscription for fossa --- .github/workflows/dep-lic-scan.yaml | 39 ----------------------------- 1 file changed, 39 deletions(-) delete mode 100644 .github/workflows/dep-lic-scan.yaml diff --git a/.github/workflows/dep-lic-scan.yaml b/.github/workflows/dep-lic-scan.yaml deleted file mode 100644 index b2a5b8e1925..00000000000 --- a/.github/workflows/dep-lic-scan.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -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@v4 - - 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 From 5771943aa2cf2fee7f98115dc4dee7da540b3bd9 Mon Sep 17 00:00:00 2001 From: Abe Ratnofsky Date: Thu, 18 Jan 2024 14:20:44 -0500 Subject: [PATCH 04/24] CASSANDRA-19180: Support reloading keystore in cassandra-java-driver --- .../api/core/config/DefaultDriverOption.java | 6 + .../api/core/config/TypedDriverOption.java | 6 + .../core/ssl/DefaultSslEngineFactory.java | 35 ++- .../core/ssl/ReloadingKeyManagerFactory.java | 257 +++++++++++++++++ core/src/main/resources/reference.conf | 7 + .../ssl/ReloadingKeyManagerFactoryTest.java | 272 ++++++++++++++++++ .../ReloadingKeyManagerFactoryTest/README.md | 39 +++ .../certs/client-alternate.keystore | Bin 0 -> 2467 bytes .../certs/client-original.keystore | Bin 0 -> 2457 bytes .../certs/client.truststore | Bin 0 -> 1002 bytes .../certs/server.keystore | Bin 0 -> 2407 bytes .../certs/server.truststore | Bin 0 -> 1890 bytes manual/core/ssl/README.md | 10 +- upgrade_guide/README.md | 11 + 14 files changed, 627 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java create mode 100644 core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/README.md create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-alternate.keystore create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-original.keystore create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client.truststore create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/server.keystore create mode 100644 core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/server.truststore 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 index 55e8d53dc66..6f907b61e37 100644 --- 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 @@ -261,6 +261,12 @@ public enum DefaultDriverOption implements DriverOption { *

Value-type: {@link String} */ SSL_KEYSTORE_PASSWORD("advanced.ssl-engine-factory.keystore-password"), + /** + * The duration between attempts to reload the keystore. + * + *

Value-type: {@link java.time.Duration} + */ + SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"), /** * The location of the truststore file. * diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java index 9be69d0424f..8f834fd96bd 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java @@ -238,6 +238,12 @@ public String toString() { /** The keystore password. */ public static final TypedDriverOption SSL_KEYSTORE_PASSWORD = new TypedDriverOption<>(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, GenericType.STRING); + + /** The duration between attempts to reload the keystore. */ + public static final TypedDriverOption SSL_KEYSTORE_RELOAD_INTERVAL = + new TypedDriverOption<>( + DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL, GenericType.DURATION); + /** The location of the truststore file. */ public static final TypedDriverOption SSL_TRUSTSTORE_PATH = new TypedDriverOption<>(DefaultDriverOption.SSL_TRUSTSTORE_PATH, GenericType.STRING); 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 index 085b36dc539..55a6e9c7da8 100644 --- 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 @@ -27,11 +27,12 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyStore; import java.security.SecureRandom; +import java.time.Duration; import java.util.List; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; @@ -54,6 +55,7 @@ * truststore-password = password123 * keystore-path = /path/to/client.keystore * keystore-password = password123 + * keystore-reload-interval = 30 minutes * } * } * @@ -66,6 +68,7 @@ public class DefaultSslEngineFactory implements SslEngineFactory { private final SSLContext sslContext; private final String[] cipherSuites; private final boolean requireHostnameValidation; + private ReloadingKeyManagerFactory kmf; /** Builds a new instance from the driver configuration. */ public DefaultSslEngineFactory(DriverContext driverContext) { @@ -132,20 +135,8 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio } // 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); - } + kmf = buildReloadingKeyManagerFactory(config); } context.init( @@ -159,8 +150,22 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio } } + private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory( + DriverExecutionProfile config) { + Path keystorePath = Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH)); + String password = + config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) + ? config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) + : null; + Duration reloadInterval = + config.isDefined(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL) + ? config.getDuration(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL) + : Duration.ZERO; + return ReloadingKeyManagerFactory.create(keystorePath, password, reloadInterval); + } + @Override public void close() throws Exception { - // nothing to do + kmf.close(); } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java new file mode 100644 index 00000000000..9aaee701114 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 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.shaded.guava.common.annotations.VisibleForTesting; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReloadingKeyManagerFactory extends KeyManagerFactory implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(ReloadingKeyManagerFactory.class); + private static final String KEYSTORE_TYPE = "JKS"; + private Path keystorePath; + private String keystorePassword; + private ScheduledExecutorService executor; + private final Spi spi; + + // We're using a single thread executor so this shouldn't need to be volatile, since all updates + // to lastDigest should come from the same thread + private volatile byte[] lastDigest; + + /** + * Create a new {@link ReloadingKeyManagerFactory} with the given keystore file and password, + * reloading from the file's content at the given interval. This function will do an initial + * reload before returning, to confirm that the file exists and is readable. + * + * @param keystorePath the keystore file to reload + * @param keystorePassword the keystore password + * @param reloadInterval the duration between reload attempts. Set to {@link + * java.time.Duration#ZERO} to disable scheduled reloading. + * @return + */ + public static ReloadingKeyManagerFactory create( + Path keystorePath, String keystorePassword, Duration reloadInterval) { + KeyManagerFactory kmf; + try { + kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + KeyStore ks; + try (InputStream ksf = Files.newInputStream(keystorePath)) { + ks = KeyStore.getInstance(KEYSTORE_TYPE); + ks.load(ksf, keystorePassword.toCharArray()); + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + try { + kmf.init(ks, keystorePassword.toCharArray()); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new RuntimeException(e); + } + + ReloadingKeyManagerFactory reloadingKeyManagerFactory = new ReloadingKeyManagerFactory(kmf); + reloadingKeyManagerFactory.start(keystorePath, keystorePassword, reloadInterval); + return reloadingKeyManagerFactory; + } + + @VisibleForTesting + protected ReloadingKeyManagerFactory(KeyManagerFactory initial) { + this( + new Spi((X509ExtendedKeyManager) initial.getKeyManagers()[0]), + initial.getProvider(), + initial.getAlgorithm()); + } + + private ReloadingKeyManagerFactory(Spi spi, Provider provider, String algorithm) { + super(spi, provider, algorithm); + this.spi = spi; + } + + private void start(Path keystorePath, String keystorePassword, Duration reloadInterval) { + this.keystorePath = keystorePath; + this.keystorePassword = keystorePassword; + this.executor = + Executors.newScheduledThreadPool( + 1, + runnable -> { + Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setDaemon(true); + return t; + }); + + // Ensure that reload is called once synchronously, to make sure the file exists etc. + reload(); + + if (!reloadInterval.isZero()) + this.executor.scheduleWithFixedDelay( + this::reload, + reloadInterval.toMillis(), + reloadInterval.toMillis(), + TimeUnit.MILLISECONDS); + } + + @VisibleForTesting + void reload() { + try { + reload0(); + } catch (Exception e) { + logger.warn("Failed to reload", e); + } + } + + private synchronized void reload0() + throws NoSuchAlgorithmException, IOException, KeyStoreException, CertificateException, + UnrecoverableKeyException { + logger.debug("Checking KeyStore file {} for updates", keystorePath); + + final byte[] keyStoreBytes = Files.readAllBytes(keystorePath); + final byte[] newDigest = digest(keyStoreBytes); + if (lastDigest != null && Arrays.equals(lastDigest, digest(keyStoreBytes))) { + logger.debug("KeyStore file content has not changed; skipping update"); + return; + } + + final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + try (InputStream inputStream = new ByteArrayInputStream(keyStoreBytes)) { + keyStore.load(inputStream, keystorePassword.toCharArray()); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keystorePassword.toCharArray()); + logger.info("Detected updates to KeyStore file {}", keystorePath); + + this.spi.keyManager.set((X509ExtendedKeyManager) kmf.getKeyManagers()[0]); + this.lastDigest = newDigest; + } + + @Override + public void close() throws Exception { + if (executor != null) { + executor.shutdown(); + } + } + + private static byte[] digest(byte[] payload) throws NoSuchAlgorithmException { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(payload); + } + + private static class Spi extends KeyManagerFactorySpi { + DelegatingKeyManager keyManager; + + Spi(X509ExtendedKeyManager initial) { + this.keyManager = new DelegatingKeyManager(initial); + } + + @Override + protected void engineInit(KeyStore ks, char[] password) { + throw new UnsupportedOperationException(); + } + + @Override + protected void engineInit(ManagerFactoryParameters spec) { + throw new UnsupportedOperationException(); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return new KeyManager[] {keyManager}; + } + } + + private static class DelegatingKeyManager extends X509ExtendedKeyManager { + AtomicReference delegate; + + DelegatingKeyManager(X509ExtendedKeyManager initial) { + delegate = new AtomicReference<>(initial); + } + + void set(X509ExtendedKeyManager keyManager) { + delegate.set(keyManager); + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return delegate.get().chooseEngineClientAlias(keyType, issuers, engine); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return delegate.get().chooseEngineServerAlias(keyType, issuers, engine); + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.get().getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return delegate.get().chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.get().getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return delegate.get().chooseServerAlias(keyType, issuers, socket); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return delegate.get().getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return delegate.get().getPrivateKey(alias); + } + } +} diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 75bed97e498..d1ac22e553b 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -790,6 +790,13 @@ datastax-java-driver { // truststore-password = password123 // keystore-path = /path/to/client.keystore // keystore-password = password123 + + # The duration between attempts to reload the keystore from the contents of the file specified + # by `keystore-path`. This is mainly relevant in environments where certificates have short + # lifetimes and applications are restarted infrequently, since an expired client certificate + # will prevent new connections from being established until the application is restarted. If + # not set, defaults to not reload the keystore. + // keystore-reload-interval = 30 minutes } # The generator that assigns a microsecond timestamp to each request. diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java new file mode 100644 index 00000000000..d291924b800 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 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 static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManagerFactory; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReloadingKeyManagerFactoryTest { + private static final Logger logger = + LoggerFactory.getLogger(ReloadingKeyManagerFactoryTest.class); + + static final Path CERT_BASE = + Paths.get( + ReloadingKeyManagerFactoryTest.class + .getResource( + String.format("/%s/certs/", ReloadingKeyManagerFactoryTest.class.getSimpleName())) + .getPath()); + static final Path SERVER_KEYSTORE_PATH = CERT_BASE.resolve("server.keystore"); + static final Path SERVER_TRUSTSTORE_PATH = CERT_BASE.resolve("server.truststore"); + + static final Path ORIGINAL_CLIENT_KEYSTORE_PATH = CERT_BASE.resolve("client-original.keystore"); + static final Path ALTERNATE_CLIENT_KEYSTORE_PATH = CERT_BASE.resolve("client-alternate.keystore"); + static final BigInteger ORIGINAL_CLIENT_KEYSTORE_CERT_SERIAL = + convertSerial("7372a966"); // 1936894310 + static final BigInteger ALTERNATE_CLIENT_KEYSTORE_CERT_SERIAL = + convertSerial("e50bf31"); // 240172849 + + // File at this path will change content + static final Path TMP_CLIENT_KEYSTORE_PATH; + + static { + try { + TMP_CLIENT_KEYSTORE_PATH = + Files.createTempFile(ReloadingKeyManagerFactoryTest.class.getSimpleName(), null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static final Path CLIENT_TRUSTSTORE_PATH = CERT_BASE.resolve("client.truststore"); + static final String CERTSTORE_PASSWORD = "changeit"; + static final Duration NO_SCHEDULED_RELOAD = Duration.ofMillis(0); + + private static TrustManagerFactory buildTrustManagerFactory() { + TrustManagerFactory tmf; + try (InputStream tsf = Files.newInputStream(CLIENT_TRUSTSTORE_PATH)) { + KeyStore ts = KeyStore.getInstance("JKS"); + char[] password = CERTSTORE_PASSWORD.toCharArray(); + ts.load(tsf, password); + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + } catch (Exception e) { + throw new RuntimeException(e); + } + return tmf; + } + + private static SSLContext buildServerSslContext() { + try { + SSLContext context = SSLContext.getInstance("SSL"); + + TrustManagerFactory tmf; + try (InputStream tsf = Files.newInputStream(SERVER_TRUSTSTORE_PATH)) { + KeyStore ts = KeyStore.getInstance("JKS"); + char[] password = CERTSTORE_PASSWORD.toCharArray(); + ts.load(tsf, password); + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + } + + KeyManagerFactory kmf; + try (InputStream ksf = Files.newInputStream(SERVER_KEYSTORE_PATH)) { + KeyStore ks = KeyStore.getInstance("JKS"); + char[] password = CERTSTORE_PASSWORD.toCharArray(); + ks.load(ksf, password); + kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, password); + } + + context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return context; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void client_certificates_should_reload() throws Exception { + Files.copy( + ORIGINAL_CLIENT_KEYSTORE_PATH, TMP_CLIENT_KEYSTORE_PATH, REPLACE_EXISTING, COPY_ATTRIBUTES); + + final BlockingQueue> peerCertificates = + new LinkedBlockingQueue<>(1); + + // Create a listening socket. Make sure there's no backlog so each accept is in order. + SSLContext serverSslContext = buildServerSslContext(); + final SSLServerSocket server = + (SSLServerSocket) serverSslContext.getServerSocketFactory().createServerSocket(); + server.bind(new InetSocketAddress(0), 1); + server.setUseClientMode(false); + server.setNeedClientAuth(true); + Thread serverThread = + new Thread( + () -> { + while (true) { + try { + logger.info("Server accepting client"); + final SSLSocket conn = (SSLSocket) server.accept(); + logger.info("Server accepted client {}", conn); + conn.addHandshakeCompletedListener( + event -> { + boolean offer; + try { + // Transfer certificates to client thread once handshake is complete, so + // it can safely close + // the socket + offer = + peerCertificates.offer( + Optional.of((X509Certificate[]) event.getPeerCertificates())); + } catch (SSLPeerUnverifiedException e) { + offer = peerCertificates.offer(Optional.empty()); + } + Assert.assertTrue(offer); + }); + logger.info("Server starting handshake"); + // Without this, client handshake blocks + conn.startHandshake(); + } catch (IOException e) { + // Not sure why I sometimes see ~thousands of these locally + if (e instanceof SocketException && e.getMessage().contains("Socket closed")) + return; + logger.info("Server accept error", e); + } + } + }); + serverThread.setName(String.format("%s-serverThread", this.getClass().getSimpleName())); + serverThread.setDaemon(true); + serverThread.start(); + + final ReloadingKeyManagerFactory kmf = + ReloadingKeyManagerFactory.create( + TMP_CLIENT_KEYSTORE_PATH, CERTSTORE_PASSWORD, NO_SCHEDULED_RELOAD); + // Need a tmf that tells the server to send its certs + final TrustManagerFactory tmf = buildTrustManagerFactory(); + + // Check original client certificate + testClientCertificates( + kmf, + tmf, + server.getLocalSocketAddress(), + () -> { + try { + return peerCertificates.poll(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, + certs -> { + Assert.assertEquals(1, certs.length); + X509Certificate cert = certs[0]; + Assert.assertEquals(ORIGINAL_CLIENT_KEYSTORE_CERT_SERIAL, cert.getSerialNumber()); + }); + + // Update keystore content + logger.info("Updating keystore file with new content"); + Files.copy( + ALTERNATE_CLIENT_KEYSTORE_PATH, + TMP_CLIENT_KEYSTORE_PATH, + REPLACE_EXISTING, + COPY_ATTRIBUTES); + kmf.reload(); + + // Check that alternate client certificate was applied + testClientCertificates( + kmf, + tmf, + server.getLocalSocketAddress(), + () -> { + try { + return peerCertificates.poll(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, + certs -> { + Assert.assertEquals(1, certs.length); + X509Certificate cert = certs[0]; + Assert.assertEquals(ALTERNATE_CLIENT_KEYSTORE_CERT_SERIAL, cert.getSerialNumber()); + }); + + kmf.close(); + server.close(); + } + + private static void testClientCertificates( + KeyManagerFactory kmf, + TrustManagerFactory tmf, + SocketAddress serverAddress, + Supplier> certsSupplier, + Consumer certsConsumer) + throws NoSuchAlgorithmException, KeyManagementException, IOException { + SSLContext clientSslContext = SSLContext.getInstance("TLS"); + clientSslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + final SSLSocket client = (SSLSocket) clientSslContext.getSocketFactory().createSocket(); + logger.info("Client connecting"); + client.connect(serverAddress); + logger.info("Client doing handshake"); + client.startHandshake(); + + final Optional lastCertificate = certsSupplier.get(); + logger.info("Client got its certificate back from the server; closing socket"); + client.close(); + Assert.assertNotNull(lastCertificate); + Assert.assertTrue(lastCertificate.isPresent()); + logger.info("Client got its certificate back from server: {}", lastCertificate); + + certsConsumer.accept(lastCertificate.get()); + } + + private static BigInteger convertSerial(String hex) { + final BigInteger serial = new BigInteger(Integer.valueOf(hex, 16).toString()); + logger.info("Serial hex {} is {}", hex, serial); + return serial; + } +} diff --git a/core/src/test/resources/ReloadingKeyManagerFactoryTest/README.md b/core/src/test/resources/ReloadingKeyManagerFactoryTest/README.md new file mode 100644 index 00000000000..9ff9b622e5b --- /dev/null +++ b/core/src/test/resources/ReloadingKeyManagerFactoryTest/README.md @@ -0,0 +1,39 @@ +# How to create cert stores for ReloadingKeyManagerFactoryTest + +Need the following cert stores: +- `server.keystore` +- `client-original.keystore` +- `client-alternate.keystore` +- `server.truststore`: trusts `client-original.keystore` and `client-alternate.keystore` +- `client.truststore`: trusts `server.keystore` + +We shouldn't need any signing requests or chains of trust, since truststores are just including certs directly. + +First create the three keystores: +``` +$ keytool -genkeypair -keyalg RSA -alias server -keystore server.keystore -dname "CN=server" -storepass changeit -keypass changeit +$ keytool -genkeypair -keyalg RSA -alias client-original -keystore client-original.keystore -dname "CN=client-original" -storepass changeit -keypass changeit +$ keytool -genkeypair -keyalg RSA -alias client-alternate -keystore client-alternate.keystore -dname "CN=client-alternate" -storepass changeit -keypass changeit +``` + +Note that we need to use `-keyalg RSA` because keytool's default keyalg is DSA, which TLS 1.3 doesn't support. If DSA is +used, the handshake will fail due to the server not being able to find any authentication schemes compatible with its +x509 certificate ("Unavailable authentication scheme"). + +Then export all the certs: +``` +$ keytool -exportcert -keystore server.keystore -alias server -file server.cert -storepass changeit +$ keytool -exportcert -keystore client-original.keystore -alias client-original -file client-original.cert -storepass changeit +$ keytool -exportcert -keystore client-alternate.keystore -alias client-alternate -file client-alternate.cert -storepass changeit +``` + +Then create the server.truststore that trusts the two client certs: +``` +$ keytool -import -file client-original.cert -alias client-original -keystore server.truststore -storepass changeit +$ keytool -import -file client-alternate.cert -alias client-alternate -keystore server.truststore -storepass changeit +``` + +Then create the client.truststore that trusts the server cert: +``` +$ keytool -import -file server.cert -alias server -keystore client.truststore -storepass changeit +``` diff --git a/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-alternate.keystore b/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-alternate.keystore new file mode 100644 index 0000000000000000000000000000000000000000..91cee636a0baab4d6e8b706c76da6bffd8398f37 GIT binary patch literal 2467 zcmY+Ec|6pK8^>qN3{8_AWE>&JwIR$TN$z_vtXZ)}Nu%63a;C=hbw!RwWSlc{Ws$S6 z?)zw#9248H$k9kO3L)WVf3M$f_qBgKujl!^pXc-Z`N05i8W$LX0pJeYaK(6B{5CI` z2TTXx=1>4`a)b>q04)4pE0`}7fO#EZx1)05M*Pop;y9R#4nX%X0CXFpz(_!+E1^f*j_q18d2Sn?o!MLvP>&51fDW$t2UVlHvQ9@L9oBPQ8A4sTlWF&YRT3u*E0+WN?r!T&q}?d|KEf^LS?aLVfMGdgs>) zdDeG(T&lkR>AznZ7kDPQmt{{~r8qf%C@nBTBdgEv+0{x833;>;&$$=wbH;@m?OAE~ z_lZ{Z2Gf{`(y{enkkglPGu5l%9!q(aI-Bm3uj28?q6UKfMha;49c4w0Q9~5^aYS^Z!Pz8jD(=dY?@B=e?!tkO zX2_`>5>KSdKikfskn;0b5OYgJUj4TYn}wNQ#;uD;R6mPpp?iKW+Ub(057Tv|Zgk-I z3J9ldZ?`==HU=N>@AbZ#^n=n3a8FvRCT(*GlP?42t_}3+@*!0?kW3{#RT|n_O4!S^ z>L<~h3Y7Fb>tEyt+&iADmEjvkz!U0ZQYkA?E95)8TgA1L4+x=}7FCAm&06FWsm-dTCS z3C!o|GYVee4{W~qg1MWrZA4@K>EXHqtA>WEB&cE6$6N_u=tjD<6>OQ0w6_g-$4|Y z!ffsmsSo5p{?4Bt47zm`Np^Q^xXa;H92~ULr@mGSmU;sF<(u%R9i%=AR$Lb<$ub?o zY&h%rr(I+DGdUrkvk-@$2Iq|(?LO2LY)yZW(np&~hmNNA0`*d6p+Rj-^HzR zg_Jdhk9x`;7Qv<)$2-&)J0zb%ONU2OGD+ItZk7D5v|BXSKlLfcMLD%73N7v`Oun`6UCnKV=c9LPO|A}*qF%!Jp_mF;W|MxHS zipQrcwGekW>i}(@byZP83q`4xz{ZdiUkr>|~P7r|2`1L_+j%C>Jn_=tSx->)hdrBZ@0BQ!QhdOOkZ7lt$_E-5icT!0N_Ckw z^~ByKk!lHAk(x2OxWA7bSZG$O-(T1~l;lxNy5fty zj8XkND&c}y>C;l68=zYtPmn9f7Ze0i29b_b(2)u_(xks84Ei4f3M&W|xa{fUDuva+ zV9#l2T+mRzh|$0Rkk-GCc(~~Rr0NKjbAdre?a4nI@V`=*`>)hR6>_mdop6;eEn6^^ zHXr-C`hTPUH+7=`h=(%PNAIePZRStSje^Mw^LgS<=-A-IEiC<@s4vMYmfFK3*oS6~ISaf^7ZcrrhN`2JM- zev+3IAaAI6c=`RA;%E2G{6~!i{KkLL9WAmE32ILpGGFdOMWa?IL8(AMXEi@rM-6hx zD>`)k+ulL#iu~f4frGb=j6%MrS8?{uqK+}D313GOIW@C%5&n5mUY^1!jX_9lwft0M znYN{V>sdej3C$!yX9YRg5b@WA_c97_^9{27I8tc(AC!|=QDhraIb_oNg!XOADd$gZ z^nnmUV}mNQ;F|9i@!ObA$Zj z{6!%d5LPC&Mx}atTNGMW*$rKB4jyOe(cFNn4dx0PoU6kx7vOj&?jA?YPp711-D!Q7 zh|~NSX)+%%^MVQPFWGbUn}GcBNlBr-(*y5QGr-BJ*b3b#iqq78CMJw?>$G~`w53;Q z!gvucIyYy&mP99{bL{FW7KKvK2p+m@3J zF>r2}(hm@>Q!o%zPy~f#DSpsZ)k-d0n@SIhTL{zjhYFs5wZbXUDbZeHHiXN*8`~c? M&hzAgfaB@^0fMoC8UO$Q literal 0 HcmV?d00001 diff --git a/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-original.keystore b/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client-original.keystore new file mode 100644 index 0000000000000000000000000000000000000000..74e31f7bc6f1ab222cf2c8ac5feb2324d1f01fdf GIT binary patch literal 2457 zcmY+EX*d)L7sqGJjAbkr5r&Z^ySZkDJF;uYmUXTrhLFaRZ3x+l24fo)vQr~lVT>)y zWQ&`mOEHm%L`4}(l##9LeV_Ne_qiX=bI$qw&pBVt^Zbz{o;(g92ub3x<>XN%TaelO zKrSGa#A5^|@#r04Z6pa2_b&+&1SUcJkMPB#@Z#kC-xePf$U!B6_mCto8>zwx{XafB zE(sQiJyu#?hE}x;Zt9+oJV^9#!tTg%fB*ybU=nyJt|{279;mNR@WY$Nh1VBYwZB4q z)vNJTDLf5-5?yZU2}#LPa#)km>Pc1rx4Fm9NHzb_yre-&<^B}C;D5^Yl5Oq zPg~(r7!&;^XWpmjpgiWfW(V$CjpbUUlk-$cL#X&Hxvx?j@i8DSHii38c~1BD5H+fY z$#CO10`N#1T1?6CRz53eU-97eLR-2UKnVZcpEHlkd^y@pvbq8mc3|VwKU&5cW^=nf zBF?RleCf9ynn00_^XA+$`l__nr4mS%qDPF^t_wQQfKx1>VpqUJF0(&}>B|+nr(@}P zkGWqs8Cn%x)JGq_66|=c(z$fOT%fBaIKM@Nk`00)-;N~dFdTLB!i9L>MAnJ>uY}Z1 z433YD(CH=HcJKJ~Pwm)f8%xvgiiZC=FoluLch{+!{P?2Lv?fElEfLkIa7C+pv$mb% z@z!w-28P1q*KySfEztD!%4%YfR}yN1V)x%WLSN`UqhMw}Q|?BXOKmNyVK1vx=6?KO zgOj@1oZiK^(o`!_2j3#w&+&=k>o?r0hfu+^%fdPhQI&jz=q=p^Wr0eyU7OOPqyJ$- zM#4_uy5v_cE3Z!x4Qj!DU3V2!i(Xz68S$z{M+!>dp)G6)rwqy070Zo11Uq6*tr7EW zWR|y&Q%Zli7`OhJ<2@x&qfTEaA(E^2HYX}pfb(qi&1!~k@57b1D~-2yIe$_g%M*?( zrB>}r+?399xdSFxtXg^R{z3>oLGrHu{d)C|Bes)&hubYkTQLzeSgCgZOz6l}d7F+Z z!LtNs`p@K-H`*%lv10>=FmEG;zzI{w`;RCm?W)F}PitE2ZZ{dE*~o&jgVJY0YKED+ z@xW5|Cn?PDBh%U{`Kc;*F8%>qyG93+&b66)P$c_` z0RJHz`=Q_A-Z>@tl$)y(x6wpXiWeliYbibQ{0)y^qlmN<`1Pc|_fjekY$o6mE}-p^ zAZxiSv4FnKU`^O=+PCkU?o9QD|*#3`Se?s3d_}{~qGvq>?~YN2r_w2slc`|0uwJc`oOFJl8HAW>o$` zgK;g8Bdyr;+9x4${m_3sH<1MLN-hjgO5KX57hO|!v3B~XmRZ(trMsy?$KPclb+$C( z(AAMn%Oa|6iTs8t=zol}O#7yI{?Ecq9+f7y4cz^{>&y@-f(lDYFUAL&<>aQ^;NQGy zl3q=%I*sa5R|dKzSjs}GDc$^P?+$JaFlQ7;jlG9L^tg`QI_T(4SsDSC7>u!@Jz=Ms zKXFEqzLw;V3xB3mkSj3WIL$)?6P{oewGD%`ubkdnG70>%0awbH>x8w;tMlC7*y4tK2Q)9 z4a^XDn8=OtR*ZPo=5YA)VJ)tQmNr?d0$&oPu}Z`QbzP>&0ljdDsH`z|CV<#xum8o* zXL@=pJ_e&gSJmNP2Z1-)f~#XrBF-t#`n2p9$2X>|sx$B-beQ!yLt09BZp+E8)bOx1 z+~xL*hR>UY-&hFWU1$r!e#BN{)wXY|8sTWfDmDlw-3>o@5sNmMc7dcsx8sJJW6 z*@3gxIQagW(SP~%VxT<4U>93(d&m4qFEc(PKs)XPaeVX5h2A@?{`W(J1mjqQK=z7U z;bi(@U5(jQ3hx@e{e5WpB2k8skUaHJ45@Ei83^sfao*da`6w{eqm^D*b~7<{1CzJZ z^LRhp(I=mIVPtOPx7}m;MU|s=nOEwprlp4+Upo&N54AX7+`3+u?>MElT@{^o*-g3X z;;#44#N#pvqFJOJbWc~bI-uX*{kc^<&EmnSLv}^p6@f%OhYyVo=#`wzcdnS!M%P({ zX8)aSUfcQF#i0Z$R)%ZS;`PgN`_@Mnx%lhC5$9gjpUmhV=L_g#VCt1Jz`7;puYN-s zB4v?0oDeku5C;qb0E>kFtX=4tCoCI=1qcLsrT_o{ literal 0 HcmV?d00001 diff --git a/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client.truststore b/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/client.truststore new file mode 100644 index 0000000000000000000000000000000000000000..3ce9a720dbc7557774ebbfbae2975235cfc52e46 GIT binary patch literal 1002 zcmV2`Yw2hW8Bt2LUiC1_~;MNQUV75 z8*8{D>rwp3j1%KTjZuVQ0s{cUP=JC1FrCLfv|Un~ufMqg!U2hEV#UbjV}nkvFlQfT zws%zX^-+$Sav!Xsm&~zLgw(N++mklJptmni0Rd*-42bRCxb#@tBkj1vJL2}Aa=vBC zPMQtbU(SfTvZUg5FrGs^!)BInqQn-V3mMPE>}8wYgV%;IL=Doa$bq*zeba|fhmr)? z{WT@X3G>&d^g3rY(d5aI5(f{}<3lUb2H{J3blG0qi^|N$X^v_Z2pUJvE)*qjm_z?h$x`Hv=IZIyBR9UJF?_KKH53{`;22J8C$gMyNS`7;2EE>X%=a@JIp|6v;!T zY;ymI?&;~1Z#4z=p%*1c$Bdb*6=c!+anhyT~Bbzz0m z_T}hw@W2;-Z>~!h?<)CRno)GNEK^>E<1jMm(BoHpZX}YC63RfgymuL$A>CPT14vHj1ohI0Pzii-mXDj|cvW60od7&MgQ|M=%~BpCiE z;EkLZ4;%5>y+OunWCmmk(MayYhI)mv6 zPQ4~K1*bj`RgbTL_>hv$Sh%;2Rb_%*VqR(zqe-TP|YJMAoGHB-b!c%=c ze(Od0212~nI#ej7BRB&JhQIk!s?ELnvSH5q`bC|Dhp*Cm`d0=z<}I|ZnD}XV86|bd z%QRlh1eb=$jG?I!bqfwlJ42C|%s%ps9{?+s!-%>%>wnm8G#-WS(ZXJ<@($?OUYlMu zpX(32Z!5lbESMj%;x@yv8K62i*e6ZzK`NCJuIBhvX`u^Jh1`Zd?f5)bQ7jb-_?oo7 zc5+Am4oT|aVMa5CE646n>jC4+;pXm)FZ@{!o>wi@SMAIht`*2+>wBH*7`Z2kTpzav zk3~EII`>{M`=uy#dP~2Gt(kSP+;pE>jRp5K^a)7@_x6S?RYO^LA1-!BWKkQ!ouu}Q{MHNp&Z2RG>1!*oDXq%oDhG; z8v@FxP5iW#rtKZQnbKXqaW&mtRHio;U%A$d`O?6UxjpfINFA~_8d2h6t)0P{^Er^2cx;GxP3321=tPI!2$=sg>+BX(XnLKY2*_au_IXLI$-tmXla zcln~;$JZlNaFPcO>2-SdUEV!#jgIIXkg#Csw(=jIib}A@W0%;@iCi~}Mz|`t^s2QK zf<5Y$VqJquw0tBy1zvovIp5noKYlRR=hD%oY9I5!BmVqTEQ z=dt$zj$UeKO|VwWUfQb?F?@egF365BQ$1=4nBv|&JIqaCG1(M;N+Oxh%JW2Hs`&S7 zfmxw!{5(15tTvPI9Z#EsM823-f79Pg#ATkXe0#v!Qg`Hmj}_)q?2RLNGTWbjRd;xe zP9bk8YsP67rk!i;x*MSvM{YNXQ)o|Y-YZ(E_>j@#WP70p+U(e!_@t7aet3;2e^$Hc zjsUQEe6lGIB|NAp;gM1J%UyTFRgR?~68d_!NR*?!+TGgxggUxCnuyF(3v_fId=C!=Zk%KYs>EXqnj-=W!|NWYY04~vh>aX&6=+HH-s#pS_yBB z6%rntQ8K_F|5haou5?nA3lIUg0tg30{natz|AYcca4?@^V8|7ek_JXeSwllz?XSX+ z*>eA0VuzNH*^+*u1P~DLbHx0U0RLrJ=wF7Nbm?oAULYOKWN&Dzcr<+%3%mXDzlJ4| z*$hOl3XRj95W|~)EE8!YGfM$kduN4Qn=YIiPXatQY}pC>&DH?HYtdAD@S~UsnFsWO zr)P^$%i)fqZAV{|p9LUkbRq#&EHO7C0my01{y02H`rI6k2im2Dor)Biy! zmugcQDoJ^*AYp8*IuuengO9zbD>F0c34CCFxI6!dHY34vq$e0_=5HyfGEuioKUM52 zQ~M~^{UArogqrE@#OX`;_^lIa2{kZ!=mifHOmZ(5l{0qTD@?Ju<%NfL(_ojAHE-;W zWLxzo7Y5Q1nYn={y)m1I6brk8f5)&5nxQt>@vnAMV zS0>C=qorZ?Ocn8*yg}3+qgl-!idDD=KNqFRwnSfTFs0W>Su+RSPvyTGJ*eP>U0}E2 zUd(Ug7hygN2wgn3-?%z;bK5X2-QWgpcg2h#D8coD{n<8>tu2zNkMava`T7YSjDTU$ z(}LEM$O%nTg6aOV?UO)V&ISQ3;`FGHBf6<#lQsIBd(`o+Q5>4x>6`tz>)7S^BAmJh9y5p&h<*DSy5s~;t8=< zR#V8GzPzlLE;Wi}gBI#}#aop(EEe+nu4MH4E&?OnilWT3#A~Jr8x+@|<9Dw*6@pQ7 zaf+R(xNR+;HpNOugkAo~P}fA-==>K|CN;=tYA>;%J`^YEI`b_jkDuKgb$tK$Sm9&B z0HDI^vbs8SH(lbKpTPpaW8*#uF(R=TPsMY29`1EBY(|DZflTAqW}w3xAC#kvsW!ps zm%$llg@{IU;uBp7(I+Q z1_p)5o@4_FK>%Pl&pd$|>s7ZHh%-Hzg^h8Zm58wi!y`cUL7^SRjK#Odlc~S&q_HPu LD}&g8$tC{*$&6Q$ literal 0 HcmV?d00001 diff --git a/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/server.truststore b/core/src/test/resources/ReloadingKeyManagerFactoryTest/certs/server.truststore new file mode 100644 index 0000000000000000000000000000000000000000..c9b06b5fbe1c2a81cfdcb7319bb82acbb0f6b4a0 GIT binary patch literal 1890 zcmV-o2c7sZf(Kp#0Ru3C2NwnjDuzgg_YDCD0ic2h2n2!$1TcaJ05F0E{00dshDe6@ z4FLxRpn?YNFoFi@0s#Opf(GIS2`Yw2hW8Bt2LUiC1_~;MNQUntF0&nl&gP8eutUG)C z)w>s(%-}4h_Gb1JtX(Gt7Lf@UC}iJX!76zfiIVH0c~njU_FTR+|0xpktwPDK`^Y9v zC5e446hb8OZ#U75j;Ms?U|uJJ;Ee>{0HK4!Z~CZ{>MP)R$nHJ8b^`v&p*PR534D#` zUX=-?!((kjc;`_o$6t3Q8Vm!@C1CabTumt4?w~^ir$fdubO0%R$@dTK@Hp~`1LsH$ ztnm<6t_iJ)*rx{1Z8$SlU#C_zKD!z+Hg5}=&!4Ad|CLESP~?q%$ZGwZiD0Z+d)JfG z{h#&*pKYW3`CuXL0oSVz-W`PbfNk0ccywE}7`4vJiNBuK!M=um?6L>23_8fv$3Xkf zla7@l0l`1gRepm3i51mk9d2a16lX)NWpbN4Nb~*UlTqHm@>p#j()|tMaG{!EK?txB zz9TXuosh|w=K7qc#CVE=bZkix_tumaFKcxM$PC9NV%fbGEf+KbT*^uz5=105W|(Gz zh1WSXjvo@eD3v^to!L5ki(e-mkiE`5xl~T2WPugTRw%T2ev{!*lYLn)`A_@qfY%1B zTfV1dpKPoyX>GX*-L)BMKNCgSIqdP;;pr1k>qN<6L6Y>N4k$Q11k5RSivFQSO~BVJ zc@jUS2tpjyl+t?636-1+=#xyVuQGI&C3jziYebkuEB%={ht9Zx7*(fy6Jlk-E!|Vk zx8C6hY3&W{elehl-nRQnklm4qmxyWM@)BS7_N*p43FLryqfhgYi6tz@qvNXwF4!^7 zqo8wN&kRPfY$$<`^%L{QuaL1Rq?#)xz-HOAiWg_(q090rB_K=e+sz)6(z+ghLOGO{ zMCk?=+l_7=Z>;vt${=nyv>CdN=1b*ZO8N@4w>g8DINf3{H{$aE4n>Z6x??J#u{T&o z%54l3cU!1JGMS5EC9e3a1Z8Y&Wp@t>lrOHcs37ug7T>pDgWMczgBy_~}LR>q0Y1Pl{9%j7O)q`KwU6Ksh zRdxKfk~-^lky#Z70A6rIeXi5CEBvr9@(ymtO%n0pEDfBK$I-g_eCFut@fHJT&~?tW ztaY3)I_gBKpC})fn3FTMdRZ=7eIz-MkNNr+V(M^b0c~Lwp zCupPHv*t-fLt_mv3as*+x+yvQH0sqjX~Z);uyx$a7ZvCkHpnuqmo{W*bFF7tFDZeW z_i~-$^{(ho^1Io^|J7>^gaBQtB{o37w+wztlzW{qg7m(d*}JjQ!hq*;4?S|medoRl zEqSS3CV58g+Aerc>21P}Efcs`T{qP?C+bV>W+L~~S$k#BSdXGOjCix%{+>yiZYhs} zYlCX#u_bUbj0RqExB>PJ$*|0HFA<8Y-)Cwrpmt6vysGvweeUqv`qwWNXi)Sj z!1SW!%WDc|3}%u8`*V~tAzXv2RP)L#Y}Gg3ciTdabi9l{SV$)t<4KkPPi&FDR-{OP zTsp#f;>qOE1u6|Ner4vS0|B^Jvf`P*5z0kJ?KHdPVKoLha>0|* zhWhKFT6?9;Xs5l|qcA=&AutIB1uG5%0vZJX1Qg`kli9U7#PfFfuLGPqc&FXlm~;db ctoVTJaCWa4Yfw+L!h=^Pt>>Pu0s{etpskvUfB*mh literal 0 HcmV?d00001 diff --git a/manual/core/ssl/README.md b/manual/core/ssl/README.md index b8aa9b89192..913c7bc6c9a 100644 --- a/manual/core/ssl/README.md +++ b/manual/core/ssl/README.md @@ -94,11 +94,13 @@ If you're using a CA, sign the client certificate with it (see the blog post lin this page). Then the nodes' truststores only need to contain the CA's certificate (which should already be the case if you've followed the steps for inter-node encryption). +`DefaultSslEngineFactory` supports client keystore reloading; see property +`advanced.ssl-engine-factory.keystore-reload-interval`. ### Driver configuration By default, the driver's SSL support is based on the JDK's built-in implementation: JSSE (Java -Secure Socket Extension),. +Secure Socket Extension). To enable it, you need to define an engine factory in the [configuration](../configuration/). @@ -126,6 +128,12 @@ datastax-java-driver { // truststore-password = password123 // keystore-path = /path/to/client.keystore // keystore-password = password123 + + # The duration between attempts to reload the keystore from the contents of the file specified + # by `keystore-path`. This is mainly relevant in environments where certificates have short + # lifetimes and applications are restarted infrequently, since an expired client certificate + # will prevent new connections from being established until the application is restarted. + // keystore-reload-interval = 30 minutes } } ``` diff --git a/upgrade_guide/README.md b/upgrade_guide/README.md index d1b6f8c8f7f..f98770a5cc8 100644 --- a/upgrade_guide/README.md +++ b/upgrade_guide/README.md @@ -19,6 +19,17 @@ under the License. ## Upgrade guide +### NEW VERSION PLACEHOLDER + +#### Keystore reloading in DefaultSslEngineFactory + +`DefaultSslEngineFactory` now includes an optional keystore reloading interval, for detecting changes in the local +client keystore file. This is relevant in environments with mTLS enabled and short-lived client certificates, especially +when an application restart might not always happen between a new keystore becoming available and the previous +keystore certificate expiring. + +This feature is disabled by default for compatibility. To enable, see `keystore-reload-interval` in `reference.conf`. + ### 4.17.0 #### Beta support for Java17 From a58618c1c3dcc972bfa44acad9d64d47978f9607 Mon Sep 17 00:00:00 2001 From: Abe Ratnofsky Date: Tue, 23 Jan 2024 16:09:35 -0500 Subject: [PATCH 05/24] PR feedback: avoid extra exception wrapping, provide thread naming, improve error messages, etc. --- .../api/core/config/DefaultDriverOption.java | 12 ++--- .../core/ssl/DefaultSslEngineFactory.java | 4 +- .../core/ssl/ReloadingKeyManagerFactory.java | 44 +++++++++---------- 3 files changed, 28 insertions(+), 32 deletions(-) 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 index 6f907b61e37..9812eab4cea 100644 --- 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 @@ -261,12 +261,6 @@ public enum DefaultDriverOption implements DriverOption { *

Value-type: {@link String} */ SSL_KEYSTORE_PASSWORD("advanced.ssl-engine-factory.keystore-password"), - /** - * The duration between attempts to reload the keystore. - * - *

Value-type: {@link java.time.Duration} - */ - SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"), /** * The location of the truststore file. * @@ -988,6 +982,12 @@ public enum DefaultDriverOption implements DriverOption { *

Value-type: boolean */ METRICS_GENERATE_AGGREGABLE_HISTOGRAMS("advanced.metrics.histograms.generate-aggregable"), + /** + * The duration between attempts to reload the keystore. + * + *

Value-type: {@link java.time.Duration} + */ + SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"), ; private final String path; 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 index 55a6e9c7da8..adf23f8e89a 100644 --- 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 @@ -150,8 +150,8 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio } } - private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory( - DriverExecutionProfile config) { + private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory(DriverExecutionProfile config) + throws Exception { Path keystorePath = Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH)); String password = config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java index 9aaee701114..540ddfd79fa 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java @@ -73,26 +73,17 @@ public class ReloadingKeyManagerFactory extends KeyManagerFactory implements Aut * @return */ public static ReloadingKeyManagerFactory create( - Path keystorePath, String keystorePassword, Duration reloadInterval) { - KeyManagerFactory kmf; - try { - kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + Path keystorePath, String keystorePassword, Duration reloadInterval) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, + CertificateException, IOException { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); KeyStore ks; try (InputStream ksf = Files.newInputStream(keystorePath)) { ks = KeyStore.getInstance(KEYSTORE_TYPE); ks.load(ksf, keystorePassword.toCharArray()); - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - try { - kmf.init(ks, keystorePassword.toCharArray()); - } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { - throw new RuntimeException(e); } + kmf.init(ks, keystorePassword.toCharArray()); ReloadingKeyManagerFactory reloadingKeyManagerFactory = new ReloadingKeyManagerFactory(kmf); reloadingKeyManagerFactory.start(keystorePath, keystorePassword, reloadInterval); @@ -115,24 +106,26 @@ private ReloadingKeyManagerFactory(Spi spi, Provider provider, String algorithm) private void start(Path keystorePath, String keystorePassword, Duration reloadInterval) { this.keystorePath = keystorePath; this.keystorePassword = keystorePassword; - this.executor = - Executors.newScheduledThreadPool( - 1, - runnable -> { - Thread t = Executors.defaultThreadFactory().newThread(runnable); - t.setDaemon(true); - return t; - }); // Ensure that reload is called once synchronously, to make sure the file exists etc. reload(); - if (!reloadInterval.isZero()) + if (!reloadInterval.isZero()) { + this.executor = + Executors.newScheduledThreadPool( + 1, + runnable -> { + Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setName(String.format("%s-%%d", this.getClass().getSimpleName())); + t.setDaemon(true); + return t; + }); this.executor.scheduleWithFixedDelay( this::reload, reloadInterval.toMillis(), reloadInterval.toMillis(), TimeUnit.MILLISECONDS); + } } @VisibleForTesting @@ -140,7 +133,10 @@ void reload() { try { reload0(); } catch (Exception e) { - logger.warn("Failed to reload", e); + String msg = + "Failed to reload KeyStore. If this continues to happen, your client may use stale identity" + + "certificates and fail to re-establish connections to Cassandra hosts."; + logger.warn(msg, e); } } From 76885fde3330ae8d3c9ed7bf5f0539cf5d3ccad5 Mon Sep 17 00:00:00 2001 From: Abe Ratnofsky Date: Fri, 2 Feb 2024 14:56:22 -0500 Subject: [PATCH 06/24] Address PR feedback: reload-interval to use Optional internally and null in config, rather than using sentinel Duration.ZERO --- .../core/ssl/DefaultSslEngineFactory.java | 14 ++++----- .../core/ssl/ReloadingKeyManagerFactory.java | 29 +++++++++++++------ .../ssl/ReloadingKeyManagerFactoryTest.java | 4 +-- 3 files changed, 27 insertions(+), 20 deletions(-) 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 index adf23f8e89a..bb95dc738c7 100644 --- 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 @@ -33,6 +33,7 @@ import java.security.SecureRandom; import java.time.Duration; import java.util.List; +import java.util.Optional; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; @@ -153,14 +154,11 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory(DriverExecutionProfile config) throws Exception { Path keystorePath = Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH)); - String password = - config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) - ? config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) - : null; - Duration reloadInterval = - config.isDefined(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL) - ? config.getDuration(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL) - : Duration.ZERO; + String password = config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, null); + Optional reloadInterval = + Optional.ofNullable( + config.getDuration(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL, null)); + return ReloadingKeyManagerFactory.create(keystorePath, password, reloadInterval); } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java index 540ddfd79fa..8a9e11bb2e9 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactory.java @@ -36,6 +36,7 @@ import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -68,12 +69,12 @@ public class ReloadingKeyManagerFactory extends KeyManagerFactory implements Aut * * @param keystorePath the keystore file to reload * @param keystorePassword the keystore password - * @param reloadInterval the duration between reload attempts. Set to {@link - * java.time.Duration#ZERO} to disable scheduled reloading. + * @param reloadInterval the duration between reload attempts. Set to {@link Optional#empty()} to + * disable scheduled reloading. * @return */ - public static ReloadingKeyManagerFactory create( - Path keystorePath, String keystorePassword, Duration reloadInterval) + static ReloadingKeyManagerFactory create( + Path keystorePath, String keystorePassword, Optional reloadInterval) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); @@ -103,14 +104,24 @@ private ReloadingKeyManagerFactory(Spi spi, Provider provider, String algorithm) this.spi = spi; } - private void start(Path keystorePath, String keystorePassword, Duration reloadInterval) { + private void start( + Path keystorePath, String keystorePassword, Optional reloadInterval) { this.keystorePath = keystorePath; this.keystorePassword = keystorePassword; // Ensure that reload is called once synchronously, to make sure the file exists etc. reload(); - if (!reloadInterval.isZero()) { + if (!reloadInterval.isPresent() || reloadInterval.get().isZero()) { + final String msg = + "KeyStore reloading is disabled. If your Cassandra cluster requires client certificates, " + + "client application restarts are infrequent, and client certificates have short lifetimes, then your client " + + "may fail to re-establish connections to Cassandra hosts. To enable KeyStore reloading, see " + + "`advanced.ssl-engine-factory.keystore-reload-interval` in reference.conf."; + logger.info(msg); + } else { + logger.info("KeyStore reloading is enabled with interval {}", reloadInterval.get()); + this.executor = Executors.newScheduledThreadPool( 1, @@ -122,8 +133,8 @@ private void start(Path keystorePath, String keystorePassword, Duration reloadIn }); this.executor.scheduleWithFixedDelay( this::reload, - reloadInterval.toMillis(), - reloadInterval.toMillis(), + reloadInterval.get().toMillis(), + reloadInterval.get().toMillis(), TimeUnit.MILLISECONDS); } } @@ -135,7 +146,7 @@ void reload() { } catch (Exception e) { String msg = "Failed to reload KeyStore. If this continues to happen, your client may use stale identity" - + "certificates and fail to re-establish connections to Cassandra hosts."; + + " certificates and fail to re-establish connections to Cassandra hosts."; logger.warn(msg, e); } } diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java index d291924b800..d07b45c21df 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java @@ -34,7 +34,6 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; -import java.time.Duration; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -86,7 +85,6 @@ public class ReloadingKeyManagerFactoryTest { static final Path CLIENT_TRUSTSTORE_PATH = CERT_BASE.resolve("client.truststore"); static final String CERTSTORE_PASSWORD = "changeit"; - static final Duration NO_SCHEDULED_RELOAD = Duration.ofMillis(0); private static TrustManagerFactory buildTrustManagerFactory() { TrustManagerFactory tmf; @@ -186,7 +184,7 @@ public void client_certificates_should_reload() throws Exception { final ReloadingKeyManagerFactory kmf = ReloadingKeyManagerFactory.create( - TMP_CLIENT_KEYSTORE_PATH, CERTSTORE_PASSWORD, NO_SCHEDULED_RELOAD); + TMP_CLIENT_KEYSTORE_PATH, CERTSTORE_PASSWORD, Optional.empty()); // Need a tmf that tells the server to send its certs final TrustManagerFactory tmf = buildTrustManagerFactory(); From fef3b2653c61d42c43338e4f061535b75a53c88d Mon Sep 17 00:00:00 2001 From: Bret McGuire Date: Tue, 6 Feb 2024 15:18:59 -0600 Subject: [PATCH 07/24] CASSANDRA-19352: Support native_transport_(address|port) + native_transport_port_ssl for DSE 6.8 (4.x edition) patch by absurdfarce; reviewed by absurdfarce and adutra for CASSANDRA-19352 --- .../core/metadata/DefaultTopologyMonitor.java | 76 ++++++-- .../metadata/DefaultTopologyMonitorTest.java | 180 ++++++++++++++++-- 2 files changed, 223 insertions(+), 33 deletions(-) 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 index 87008b05cec..f3dc988cfbc 100644 --- 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 @@ -34,6 +34,7 @@ import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; 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 com.datastax.oss.protocol.internal.ProtocolConstants; import com.datastax.oss.protocol.internal.response.Error; import edu.umd.cs.findbugs.annotations.NonNull; @@ -69,6 +70,10 @@ public class DefaultTopologyMonitor implements TopologyMonitor { // Assume topology queries never need paging private static final int INFINITE_PAGE_SIZE = -1; + // A few system.peers columns which get special handling below + private static final String NATIVE_PORT = "native_port"; + private static final String NATIVE_TRANSPORT_PORT = "native_transport_port"; + private final String logPrefix; private final InternalDriverContext context; private final ControlConnection controlConnection; @@ -494,28 +499,65 @@ private void savePort(DriverChannel channel) { @Nullable protected InetSocketAddress getBroadcastRpcAddress( @NonNull AdminRow row, @NonNull EndPoint localEndPoint) { - // in system.peers or system.local - InetAddress broadcastRpcInetAddress = row.getInetAddress("rpc_address"); + + InetAddress broadcastRpcInetAddress = null; + Iterator addrCandidates = + Iterators.forArray( + // in system.peers_v2 (Cassandra >= 4.0) + "native_address", + // DSE 6.8 introduced native_transport_address and native_transport_port for the + // listen address. + "native_transport_address", + // in system.peers or system.local + "rpc_address"); + + while (broadcastRpcInetAddress == null && addrCandidates.hasNext()) + broadcastRpcInetAddress = row.getInetAddress(addrCandidates.next()); + // This could only happen if system tables are corrupted, but handle gracefully if (broadcastRpcInetAddress == null) { - // in system.peers_v2 (Cassandra >= 4.0) - broadcastRpcInetAddress = row.getInetAddress("native_address"); - if (broadcastRpcInetAddress == null) { - // This could only happen if system tables are corrupted, but handle gracefully - return null; + LOG.warn( + "[{}] Unable to determine broadcast RPC IP address, returning null. " + + "This is likely due to a misconfiguration or invalid system tables. " + + "Please validate the contents of system.local and/or {}.", + logPrefix, + getPeerTableName()); + return null; + } + + Integer broadcastRpcPort = null; + Iterator portCandidates = + Iterators.forArray( + // in system.peers_v2 (Cassandra >= 4.0) + NATIVE_PORT, + // DSE 6.8 introduced native_transport_address and native_transport_port for the + // listen address. + NATIVE_TRANSPORT_PORT, + // system.local for Cassandra >= 4.0 + "rpc_port"); + + while ((broadcastRpcPort == null || broadcastRpcPort == 0) && portCandidates.hasNext()) { + + String colName = portCandidates.next(); + broadcastRpcPort = row.getInteger(colName); + // Support override for SSL port (if enabled) in DSE + if (NATIVE_TRANSPORT_PORT.equals(colName) && context.getSslEngineFactory().isPresent()) { + + String sslColName = colName + "_ssl"; + broadcastRpcPort = row.getInteger(sslColName); } } - // system.local for Cassandra >= 4.0 - Integer broadcastRpcPort = row.getInteger("rpc_port"); + // use the default port if no port information was found in the row; + // note that in rare situations, the default port might not be known, in which case we + // report zero, as advertised in the javadocs of Node and NodeInfo. if (broadcastRpcPort == null || broadcastRpcPort == 0) { - // system.peers_v2 - broadcastRpcPort = row.getInteger("native_port"); - if (broadcastRpcPort == null || broadcastRpcPort == 0) { - // use the default port if no port information was found in the row; - // note that in rare situations, the default port might not be known, in which case we - // report zero, as advertised in the javadocs of Node and NodeInfo. - broadcastRpcPort = port == -1 ? 0 : port; - } + + LOG.warn( + "[{}] Unable to determine broadcast RPC port. " + + "Trying to fall back to port used by the control connection.", + logPrefix); + broadcastRpcPort = port == -1 ? 0 : port; } + InetSocketAddress broadcastRpcAddress = new InetSocketAddress(broadcastRpcInetAddress, broadcastRpcPort); if (row.contains("peer") && broadcastRpcAddress.equals(localEndPoint.resolve())) { 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 index cc275eb1624..dd40f233518 100644 --- 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 @@ -38,6 +38,7 @@ 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.ssl.SslEngineFactory; 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; @@ -50,9 +51,11 @@ 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 com.datastax.oss.driver.shaded.guava.common.collect.Maps; import com.datastax.oss.protocol.internal.Message; import com.datastax.oss.protocol.internal.ProtocolConstants; import com.datastax.oss.protocol.internal.response.Error; +import com.google.common.collect.Streams; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; @@ -95,6 +98,8 @@ public class DefaultTopologyMonitorTest { @Mock private Appender appender; @Captor private ArgumentCaptor loggingEventCaptor; + @Mock private SslEngineFactory sslEngineFactory; + private DefaultNode node1; private DefaultNode node2; @@ -414,18 +419,6 @@ public void should_skip_invalid_peers_row_v2(String columnToCheck) { + "This is likely a gossip or snitch issue, this node will be ignored."); } - @DataProvider - public static Object[][] columnsToCheckV1() { - return new Object[][] {{"rpc_address"}, {"host_id"}, {"data_center"}, {"rack"}, {"tokens"}}; - } - - @DataProvider - public static Object[][] columnsToCheckV2() { - return new Object[][] { - {"native_address"}, {"native_port"}, {"host_id"}, {"data_center"}, {"rack"}, {"tokens"} - }; - } - @Test public void should_stop_executing_queries_once_closed() { // Given @@ -443,9 +436,9 @@ public void should_stop_executing_queries_once_closed() { public void should_warn_when_control_host_found_in_system_peers() { // Given AdminRow local = mockLocalRow(1, node1.getHostId()); - AdminRow peer3 = mockPeersRow(3, UUID.randomUUID()); - AdminRow peer2 = mockPeersRow(2, node2.getHostId()); AdminRow peer1 = mockPeersRow(1, node2.getHostId()); // invalid + AdminRow peer2 = mockPeersRow(2, node2.getHostId()); + AdminRow peer3 = mockPeersRow(3, UUID.randomUUID()); topologyMonitor.stubQueries( new StubbedQuery("SELECT * FROM system.local", mockResult(local)), new StubbedQuery("SELECT * FROM system.peers_v2", Collections.emptyMap(), null, true), @@ -462,7 +455,7 @@ public void should_warn_when_control_host_found_in_system_peers() { .hasSize(3) .extractingResultOf("getEndPoint") .containsOnlyOnce(node1.getEndPoint())); - assertLog( + assertLogContains( Level.WARN, "[null] Control node /127.0.0.1:9042 has an entry for itself in system.peers: " + "this entry will be ignored. This is likely due to a misconfiguration; " @@ -492,7 +485,7 @@ public void should_warn_when_control_host_found_in_system_peers_v2() { .hasSize(3) .extractingResultOf("getEndPoint") .containsOnlyOnce(node1.getEndPoint())); - assertLog( + assertLogContains( Level.WARN, "[null] Control node /127.0.0.1:9042 has an entry for itself in system.peers_v2: " + "this entry will be ignored. This is likely due to a misconfiguration; " @@ -500,6 +493,116 @@ public void should_warn_when_control_host_found_in_system_peers_v2() { + "all nodes in your cluster."); } + // Confirm the base case of extracting peer info from peers_v2, no SSL involved + @Test + public void should_get_peer_address_info_peers_v2() { + // Given + AdminRow local = mockLocalRow(1, node1.getHostId()); + AdminRow peer2 = mockPeersV2Row(3, node2.getHostId()); + AdminRow peer1 = mockPeersV2Row(2, node1.getHostId()); + topologyMonitor.isSchemaV2 = true; + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.local", mockResult(local)), + new StubbedQuery("SELECT * FROM system.peers_v2", mockResult(peer2, peer1))); + when(context.getSslEngineFactory()).thenReturn(Optional.empty()); + + // When + CompletionStage> futureInfos = topologyMonitor.refreshNodeList(); + + // Then + assertThatStage(futureInfos) + .isSuccess( + infos -> { + Iterator iterator = infos.iterator(); + // First NodeInfo is for local, skip past that + iterator.next(); + NodeInfo peer2nodeInfo = iterator.next(); + assertThat(peer2nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.3", 9042)); + NodeInfo peer1nodeInfo = iterator.next(); + assertThat(peer1nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.2", 9042)); + }); + } + + // Confirm the base case of extracting peer info from DSE peers table, no SSL involved + @Test + public void should_get_peer_address_info_peers_dse() { + // Given + AdminRow local = mockLocalRow(1, node1.getHostId()); + AdminRow peer2 = mockPeersRowDse(3, node2.getHostId()); + AdminRow peer1 = mockPeersRowDse(2, node1.getHostId()); + topologyMonitor.isSchemaV2 = true; + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.local", mockResult(local)), + new StubbedQuery("SELECT * FROM system.peers_v2", Maps.newHashMap(), null, true), + new StubbedQuery("SELECT * FROM system.peers", mockResult(peer2, peer1))); + when(context.getSslEngineFactory()).thenReturn(Optional.empty()); + + // When + CompletionStage> futureInfos = topologyMonitor.refreshNodeList(); + + // Then + assertThatStage(futureInfos) + .isSuccess( + infos -> { + Iterator iterator = infos.iterator(); + // First NodeInfo is for local, skip past that + iterator.next(); + NodeInfo peer2nodeInfo = iterator.next(); + assertThat(peer2nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.3", 9042)); + NodeInfo peer1nodeInfo = iterator.next(); + assertThat(peer1nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.2", 9042)); + }); + } + + // Confirm the base case of extracting peer info from DSE peers table, this time with SSL + @Test + public void should_get_peer_address_info_peers_dse_with_ssl() { + // Given + AdminRow local = mockLocalRow(1, node1.getHostId()); + AdminRow peer2 = mockPeersRowDseWithSsl(3, node2.getHostId()); + AdminRow peer1 = mockPeersRowDseWithSsl(2, node1.getHostId()); + topologyMonitor.isSchemaV2 = true; + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.local", mockResult(local)), + new StubbedQuery("SELECT * FROM system.peers_v2", Maps.newHashMap(), null, true), + new StubbedQuery("SELECT * FROM system.peers", mockResult(peer2, peer1))); + when(context.getSslEngineFactory()).thenReturn(Optional.of(sslEngineFactory)); + + // When + CompletionStage> futureInfos = topologyMonitor.refreshNodeList(); + + // Then + assertThatStage(futureInfos) + .isSuccess( + infos -> { + Iterator iterator = infos.iterator(); + // First NodeInfo is for local, skip past that + iterator.next(); + NodeInfo peer2nodeInfo = iterator.next(); + assertThat(peer2nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.3", 9043)); + NodeInfo peer1nodeInfo = iterator.next(); + assertThat(peer1nodeInfo.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.2", 9043)); + }); + } + + @DataProvider + public static Object[][] columnsToCheckV1() { + return new Object[][] {{"rpc_address"}, {"host_id"}, {"data_center"}, {"rack"}, {"tokens"}}; + } + + @DataProvider + public static Object[][] columnsToCheckV2() { + return new Object[][] { + {"native_address"}, {"native_port"}, {"host_id"}, {"data_center"}, {"rack"}, {"tokens"} + }; + } + /** Mocks the query execution logic. */ private static class TestTopologyMonitor extends DefaultTopologyMonitor { @@ -641,6 +744,43 @@ private AdminRow mockPeersV2Row(int i, UUID hostId) { } } + // Mock row for DSE ~6.8 + private AdminRow mockPeersRowDse(int i, UUID hostId) { + try { + AdminRow row = mock(AdminRow.class); + when(row.contains("peer")).thenReturn(true); + when(row.isNull("data_center")).thenReturn(false); + when(row.getString("data_center")).thenReturn("dc" + i); + when(row.getString("dse_version")).thenReturn("6.8.30"); + when(row.contains("graph")).thenReturn(true); + when(row.isNull("host_id")).thenReturn(hostId == null); + when(row.getUuid("host_id")).thenReturn(hostId); + when(row.getInetAddress("peer")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.isNull("rack")).thenReturn(false); + when(row.getString("rack")).thenReturn("rack" + i); + when(row.isNull("native_transport_address")).thenReturn(false); + when(row.getInetAddress("native_transport_address")) + .thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.isNull("native_transport_port")).thenReturn(false); + when(row.getInteger("native_transport_port")).thenReturn(9042); + when(row.isNull("tokens")).thenReturn(false); + when(row.getSetOfString("tokens")).thenReturn(ImmutableSet.of("token" + i)); + when(row.isNull("rpc_address")).thenReturn(false); + + return row; + } catch (UnknownHostException e) { + fail("unexpected", e); + return null; + } + } + + private AdminRow mockPeersRowDseWithSsl(int i, UUID hostId) { + AdminRow row = mockPeersRowDse(i, hostId); + when(row.isNull("native_transport_port_ssl")).thenReturn(false); + when(row.getInteger("native_transport_port_ssl")).thenReturn(9043); + return row; + } + private AdminResult mockResult(AdminRow... rows) { AdminResult result = mock(AdminResult.class); when(result.iterator()).thenReturn(Iterators.forArray(rows)); @@ -654,4 +794,12 @@ private void assertLog(Level level, String message) { assertThat(logs).hasSize(1); assertThat(logs.iterator().next().getFormattedMessage()).contains(message); } + + private void assertLogContains(Level level, String message) { + verify(appender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + Iterable logs = + filter(loggingEventCaptor.getAllValues()).with("level", level).get(); + assertThat( + Streams.stream(logs).map(ILoggingEvent::getFormattedMessage).anyMatch(message::contains)); + } } From 866e3510be7314b9e7ae35b5d7ae7f925b541996 Mon Sep 17 00:00:00 2001 From: Andy Tolbert <6889771+tolbertam@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:21:02 -0600 Subject: [PATCH 08/24] Replace uses of AttributeKey.newInstance The java driver uses netty channel attributes to decorate a connection's channel with the cluster name (returned from the system.local table) and the map from the OPTIONS response, both of which are obtained on connection initialization. There's an issue here that I wouldn't expect to see in practice in that the AttributeKey's used are created using AttributeKey.newInstance, which throws an exception if an AttributeKey of that name is defined anywhere else in evaluated code. This change attempts to resolve this issue by changing AttributeKey initialiation in DriverChannel from newInstance to valueOf, which avoids throwing an exception if an AttributeKey of the same name was previously instantiated. patch by Andy Tolbert; reviewed by Bret McGuire, Alexandre Dutra, Abe Ratnofsky for CASSANDRA-19290 --- .../oss/driver/internal/core/channel/DriverChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e1f00a0cd8a..6ea4d21f90f 100644 --- 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 @@ -58,7 +58,7 @@ @ThreadSafe public class DriverChannel { - static final AttributeKey CLUSTER_NAME_KEY = AttributeKey.newInstance("cluster_name"); + static final AttributeKey CLUSTER_NAME_KEY = AttributeKey.valueOf("cluster_name"); static final AttributeKey>> OPTIONS_KEY = AttributeKey.newInstance("options"); static final AttributeKey SHARDING_INFO_KEY = From 63fae96d132420801f99fa75c2ddb3a194b3842e Mon Sep 17 00:00:00 2001 From: Ekaterina Dimitrova Date: Mon, 29 Jan 2024 14:07:59 -0500 Subject: [PATCH 09/24] Fix data corruption in VectorCodec when using heap buffers patch by Ekaterina Dimitrova; reviewed by Alexandre Dutra and Bret McGuire for CASSANDRA-19333 --- .../internal/core/type/codec/VectorCodec.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VectorCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VectorCodec.java index 1b663a29d9e..2c4d2200b13 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VectorCodec.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VectorCodec.java @@ -127,17 +127,19 @@ Elements should at least precede themselves with their size (along the lines of cqlType.getDimensions(), bytes.remaining())); } + ByteBuffer slice = bytes.slice(); List rv = new ArrayList(cqlType.getDimensions()); for (int i = 0; i < cqlType.getDimensions(); ++i) { - ByteBuffer slice = bytes.slice(); - slice.limit(elementSize); + // Set the limit for the current element + int originalPosition = slice.position(); + slice.limit(originalPosition + elementSize); rv.add(this.subtypeCodec.decode(slice, protocolVersion)); - bytes.position(bytes.position() + elementSize); + // Move to the start of the next element + slice.position(originalPosition + elementSize); + // Reset the limit to the end of the buffer + slice.limit(slice.capacity()); } - /* Restore the input ByteBuffer to its original state */ - bytes.rewind(); - return CqlVector.newInstance(rv); } From 4a35de9eb4af5fa3639ab225846120bf18ce6013 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 28 Mar 2024 15:37:22 -0500 Subject: [PATCH 10/24] CASSANDRA-19504: Improve state management for Java versions in Jenkinsfile patch by Bret McGuire; reviewed by Bret McGuire for CASSANDRA-19504 --- Jenkinsfile | 19 ++++++++++--------- .../internal/core/channel/DriverChannel.java | 4 ++-- pom.xml | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c8247769631..8d2b74c5b08 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -61,12 +61,6 @@ def initializeEnvironment() { . ${JABBA_SHELL} jabba which 1.8''', returnStdout: true).trim() - env.TEST_JAVA_HOME = sh(label: 'Get TEST_JAVA_HOME',script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba which ${JABBA_VERSION}''', returnStdout: true).trim() - env.TEST_JAVA_VERSION = sh(label: 'Get TEST_JAVA_VERSION',script: '''#!/bin/bash -le - echo "${JABBA_VERSION##*.}"''', returnStdout: true).trim() - sh label: 'Download Apache CassandraⓇ or DataStax Enterprise',script: '''#!/bin/bash -le . ${JABBA_SHELL} jabba use 1.8 @@ -115,7 +109,12 @@ def buildDriver(jabbaVersion) { } def executeTests() { - sh label: 'Execute tests', script: '''#!/bin/bash -le + def testJavaHome = sh(label: 'Get TEST_JAVA_HOME',script: '''#!/bin/bash -le + . ${JABBA_SHELL} + jabba which ${JABBA_VERSION}''', returnStdout: true).trim() + def testJavaVersion = (JABBA_VERSION =~ /.*\.(\d+)/)[0][1] + + def executeTestScript = '''#!/bin/bash -le # Load CCM environment variables set -o allexport . ${HOME}/environment.txt @@ -137,8 +136,8 @@ def executeTests() { printenv | sort mvn -B -V ${INTEGRATION_TESTS_FILTER_ARGUMENT} -T 1 verify \ - -Ptest-jdk-${TEST_JAVA_VERSION} \ - -DtestJavaHome=${TEST_JAVA_HOME} \ + -Ptest-jdk-'''+testJavaVersion+''' \ + -DtestJavaHome='''+testJavaHome+''' \ -DfailIfNoTests=false \ -Dmaven.test.failure.ignore=true \ -Dmaven.javadoc.skip=${SKIP_JAVADOCS} \ @@ -149,6 +148,8 @@ def executeTests() { ${ISOLATED_ITS_ARGUMENT} \ ${PARALLELIZABLE_ITS_ARGUMENT} ''' + echo "Invoking Maven with parameters test-jdk-${testJavaVersion} and testJavaHome = ${testJavaHome}" + sh label: 'Execute tests', script: executeTestScript } def executeCodeCoverage() { 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 index 6ea4d21f90f..c7913660cbb 100644 --- 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 @@ -60,9 +60,9 @@ public class DriverChannel { static final AttributeKey CLUSTER_NAME_KEY = AttributeKey.valueOf("cluster_name"); static final AttributeKey>> OPTIONS_KEY = - AttributeKey.newInstance("options"); + AttributeKey.valueOf("options"); static final AttributeKey SHARDING_INFO_KEY = - AttributeKey.newInstance("sharding_info"); + AttributeKey.valueOf("sharding_info"); static final AttributeKey LWT_INFO_KEY = AttributeKey.newInstance("lwt_info"); @SuppressWarnings("RedundantStringConstructorCall") diff --git a/pom.xml b/pom.xml index 3fde3ac8233..f91fba3ad59 100644 --- a/pom.xml +++ b/pom.xml @@ -686,6 +686,7 @@ maven-surefire-plugin + ${testing.jvm}/bin/java ${project.basedir}/src/test/resources/logback-test.xml From e074f3f777e0e7340608d90b97dee7939ed17226 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Mon, 8 Apr 2024 11:00:46 -0500 Subject: [PATCH 11/24] Update link to GITHUB instead of JIRA --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ba5adf5f00..64a3bd992fe 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,14 @@ See the [upgrade guide](upgrade_guide/) for details. * [Manual](manual/) * [API docs] -* Bug tracking: [JIRA] +* Bug tracking: [GITHUB] * [Mailing list] * Training: [Scylla University] * [Changelog] * [FAQ] [API docs]: https://java-driver.docs.scylladb.com/scylla-4.17.0.x/api/overview-summary.html +[GITHUB]: https://github.com/scylladb/java-driver/issues [Scylla University]: https://university.scylladb.com [Changelog]: changelog/ [FAQ]: faq/ From d76df00b7aed99dfddefdedeeaae3992d66dd3b4 Mon Sep 17 00:00:00 2001 From: Ammar Khaku Date: Thu, 14 Mar 2024 16:55:59 -0700 Subject: [PATCH 12/24] CASSANDRA-19468 Don't swallow exception during metadata refresh If an exception was thrown while getting new metadata as part of schema refresh it died on the admin executor instead of being propagated to the CompletableFuture argument. Instead, catch those exceptions and hand them off to the CompletableFuture. patch by Ammar Khaku; reviewed by Chris Lohfink, Bret McGuire for CASSANDRA-19468 --- .../core/metadata/MetadataManager.java | 53 ++++++++++--------- .../core/metadata/MetadataManagerTest.java | 23 ++++++++ 2 files changed, 52 insertions(+), 24 deletions(-) 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 index 7aa2fb13bcd..1c53dd49e62 100644 --- 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 @@ -447,30 +447,35 @@ private void startSchemaRequest(CompletableFuture refreshFu if (agreementError != null) { refreshFuture.completeExceptionally(agreementError); } else { - schemaQueriesFactory - .newInstance() - .execute() - .thenApplyAsync(this::parseAndApplySchemaRows, adminExecutor) - .whenComplete( - (newMetadata, metadataError) -> { - if (metadataError != null) { - refreshFuture.completeExceptionally(metadataError); - } else { - refreshFuture.complete( - new RefreshSchemaResult(newMetadata, schemaInAgreement)); - } - - firstSchemaRefreshFuture.complete(null); - - currentSchemaRefresh = null; - // If another refresh was enqueued during this one, run it now - if (queuedSchemaRefresh != null) { - CompletableFuture tmp = - this.queuedSchemaRefresh; - this.queuedSchemaRefresh = null; - startSchemaRequest(tmp); - } - }); + try { + schemaQueriesFactory + .newInstance() + .execute() + .thenApplyAsync(this::parseAndApplySchemaRows, adminExecutor) + .whenComplete( + (newMetadata, metadataError) -> { + if (metadataError != null) { + refreshFuture.completeExceptionally(metadataError); + } else { + refreshFuture.complete( + new RefreshSchemaResult(newMetadata, schemaInAgreement)); + } + + firstSchemaRefreshFuture.complete(null); + + currentSchemaRefresh = null; + // If another refresh was enqueued during this one, run it now + if (queuedSchemaRefresh != null) { + CompletableFuture tmp = + this.queuedSchemaRefresh; + this.queuedSchemaRefresh = null; + startSchemaRequest(tmp); + } + }); + } catch (Throwable t) { + LOG.debug("[{}] Exception getting new metadata", logPrefix, t); + refreshFuture.completeExceptionally(t); + } } }); } else if (queuedSchemaRefresh == null) { 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 index 460f99abd85..375209d9fcf 100644 --- 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 @@ -20,6 +20,7 @@ import static com.datastax.oss.driver.Assertions.assertThat; import static com.datastax.oss.driver.Assertions.assertThatStage; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -33,6 +34,7 @@ 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.schema.parsing.SchemaParserFactory; import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaQueriesFactory; import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; @@ -64,6 +66,7 @@ public class MetadataManagerTest { @Mock private InternalDriverContext context; @Mock private NettyOptions nettyOptions; + @Mock private ControlConnection controlConnection; @Mock private TopologyMonitor topologyMonitor; @Mock private DriverConfig config; @Mock private DriverExecutionProfile defaultProfile; @@ -85,6 +88,7 @@ public void setup() { when(context.getNettyOptions()).thenReturn(nettyOptions); when(context.getTopologyMonitor()).thenReturn(topologyMonitor); + when(context.getControlConnection()).thenReturn(controlConnection); when(defaultProfile.getDuration(DefaultDriverOption.METADATA_SCHEMA_WINDOW)) .thenReturn(Duration.ZERO); @@ -286,6 +290,25 @@ public void should_remove_node() { assertThat(refresh.broadcastRpcAddressToRemove).isEqualTo(broadcastRpcAddress2); } + @Test + public void refreshSchema_should_work() { + // Given + IllegalStateException expectedException = new IllegalStateException("Error we're testing"); + when(schemaQueriesFactory.newInstance()).thenThrow(expectedException); + when(topologyMonitor.refreshNodeList()).thenReturn(CompletableFuture.completedFuture(ImmutableList.of(mock(NodeInfo.class)))); + when(topologyMonitor.checkSchemaAgreement()).thenReturn(CompletableFuture.completedFuture(Boolean.TRUE)); + when(controlConnection.init(anyBoolean(), anyBoolean(), anyBoolean())).thenReturn(CompletableFuture.completedFuture(null)); + metadataManager.refreshNodes(); // required internal state setup for this + waitForPendingAdminTasks(() -> metadataManager.refreshes.size() == 1); // sanity check + + // When + CompletionStage result = metadataManager.refreshSchema("foo", true, true); + + // Then + waitForPendingAdminTasks(() -> result.toCompletableFuture().isDone()); + assertThatStage(result).isFailed(t -> assertThat(t).isEqualTo(expectedException)); + } + private static class TestMetadataManager extends MetadataManager { private List refreshes = new CopyOnWriteArrayList<>(); From 201f6131d27faf9ea9f1eeb74fe0541b66ba21d1 Mon Sep 17 00:00:00 2001 From: janehe Date: Fri, 12 Apr 2024 13:20:33 -0700 Subject: [PATCH 13/24] patch by Jane He; reviewed by Alexandre Dutra and Bret McGuire for CASSANDRA-19457 --- .../core/metrics/AbstractMetricUpdater.java | 2 -- .../core/metrics/DropwizardMetricUpdater.java | 2 +- .../internal/core/metrics/MetricUpdater.java | 2 ++ .../core/metrics/NoopNodeMetricUpdater.java | 5 +++++ .../core/metrics/NoopSessionMetricUpdater.java | 3 +++ .../internal/core/session/DefaultSession.java | 15 +++++++++++++++ .../driver/core/metrics/DropwizardMetricsIT.java | 6 ++++++ .../oss/driver/core/metrics/MetricsITBase.java | 10 ++++++++-- .../metrics/micrometer/MicrometerMetricsIT.java | 6 ++++++ .../microprofile/MicroProfileMetricsIT.java | 6 ++++++ .../micrometer/MicrometerMetricUpdater.java | 2 +- .../microprofile/MicroProfileMetricUpdater.java | 2 +- 12 files changed, 54 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/AbstractMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/AbstractMetricUpdater.java index fcfe56b605e..5e2392a2e7f 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/AbstractMetricUpdater.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/AbstractMetricUpdater.java @@ -180,6 +180,4 @@ protected Timeout newTimeout() { expireAfter.toNanos(), TimeUnit.NANOSECONDS); } - - protected abstract void clearMetrics(); } 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 index 8590917be21..9377fb3a17e 100644 --- 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 @@ -91,7 +91,7 @@ public void updateTimer( } @Override - protected void clearMetrics() { + public void clearMetrics() { for (MetricT metric : metrics.keySet()) { MetricId id = getMetricId(metric); registry.remove(id.getName()); 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 index c4b432f3c50..c07d1b136af 100644 --- 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 @@ -46,4 +46,6 @@ default void markMeter(MetricT metric, @Nullable String profileName) { void updateTimer(MetricT metric, @Nullable String profileName, long duration, TimeUnit unit); boolean isEnabled(MetricT metric, @Nullable String profileName); + + void clearMetrics(); } 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 index 45f0797c7b5..8d216990331 100644 --- 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 @@ -53,4 +53,9 @@ public boolean isEnabled(NodeMetric metric, String profileName) { // since methods don't do anything, return false return false; } + + @Override + public void clearMetrics() { + // nothing to do + } } 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 index 1666261590c..7099a8ddcac 100644 --- 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 @@ -53,4 +53,7 @@ public boolean isEnabled(SessionMetric metric, String profileName) { // since methods don't do anything, return false return false; } + + @Override + public void clearMetrics() {} } 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 index 50ebca151ee..17516cef390 100644 --- 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 @@ -41,6 +41,7 @@ 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.metadata.DefaultNode; import com.datastax.oss.driver.internal.core.metadata.MetadataManager; import com.datastax.oss.driver.internal.core.metadata.MetadataManager.RefreshSchemaResult; import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; @@ -568,6 +569,13 @@ private void close() { closePolicies(); + // clear metrics to prevent memory leak + for (Node n : metadataManager.getMetadata().getNodes().values()) { + ((DefaultNode) n).getMetricUpdater().clearMetrics(); + } + + DefaultSession.this.metricUpdater.clearMetrics(); + List> childrenCloseStages = new ArrayList<>(); for (AsyncAutoCloseable closeable : internalComponentsToClose()) { childrenCloseStages.add(closeable.closeAsync()); @@ -587,6 +595,13 @@ private void forceClose() { logPrefix, (closeWasCalled ? "" : "not ")); + // clear metrics to prevent memory leak + for (Node n : metadataManager.getMetadata().getNodes().values()) { + ((DefaultNode) n).getMetricUpdater().clearMetrics(); + } + + DefaultSession.this.metricUpdater.clearMetrics(); + if (closeWasCalled) { // onChildrenClosed has already been scheduled for (AsyncAutoCloseable closeable : internalComponentsToClose()) { diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/DropwizardMetricsIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/DropwizardMetricsIT.java index 6cbe443f2a6..e0184516e21 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/DropwizardMetricsIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/DropwizardMetricsIT.java @@ -198,6 +198,12 @@ protected void assertNodeMetricsNotEvicted(CqlSession session, Node node) { } } + @Override + protected void assertMetricsNotPresent(Object registry) { + MetricRegistry dropwizardRegistry = (MetricRegistry) registry; + assertThat(dropwizardRegistry.getMetrics()).isEmpty(); + } + @Override protected void assertNodeMetricsEvicted(CqlSession session, Node node) { InternalDriverContext context = (InternalDriverContext) session.getContext(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/MetricsITBase.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/MetricsITBase.java index 7fac3f98f52..e6121217619 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/MetricsITBase.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metrics/MetricsITBase.java @@ -83,8 +83,10 @@ public void resetSimulacron() { @Test @UseDataProvider("descriptorsAndPrefixes") - public void should_expose_metrics_if_enabled(Class metricIdGenerator, String prefix) { + public void should_expose_metrics_if_enabled_and_clear_metrics_if_closed( + Class metricIdGenerator, String prefix) { + Object registry = newMetricRegistry(); Assume.assumeFalse( "Cannot use metric tags with Dropwizard", metricIdGenerator.getSimpleName().contains("Tagging") @@ -101,12 +103,14 @@ public void should_expose_metrics_if_enabled(Class metricIdGenerator, String CqlSession.builder() .addContactEndPoints(simulacron().getContactPoints()) .withConfigLoader(loader) - .withMetricRegistry(newMetricRegistry()) + .withMetricRegistry(registry) .build()) { session.prepare("irrelevant"); queryAllNodes(session); assertMetricsPresent(session); + } finally { + assertMetricsNotPresent(registry); } } @@ -262,4 +266,6 @@ private DefaultNode findNode(CqlSession session, int id) { return (DefaultNode) session.getMetadata().findNode(address1).orElseThrow(IllegalStateException::new); } + + protected abstract void assertMetricsNotPresent(Object registry); } diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/metrics/micrometer/MicrometerMetricsIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/metrics/micrometer/MicrometerMetricsIT.java index 8f9c0a801d5..ef87040f2a2 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/metrics/micrometer/MicrometerMetricsIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/metrics/micrometer/MicrometerMetricsIT.java @@ -188,6 +188,12 @@ protected void assertNodeMetricsNotEvicted(CqlSession session, Node node) { } } + @Override + protected void assertMetricsNotPresent(Object registry) { + MeterRegistry micrometerRegistry = (MeterRegistry) registry; + assertThat(micrometerRegistry.getMeters()).isEmpty(); + } + @Override protected void assertNodeMetricsEvicted(CqlSession session, Node node) { InternalDriverContext context = (InternalDriverContext) session.getContext(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/metrics/microprofile/MicroProfileMetricsIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/metrics/microprofile/MicroProfileMetricsIT.java index 1294be3deae..aa04c058a49 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/metrics/microprofile/MicroProfileMetricsIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/metrics/microprofile/MicroProfileMetricsIT.java @@ -188,6 +188,12 @@ protected void assertNodeMetricsNotEvicted(CqlSession session, Node node) { } } + @Override + protected void assertMetricsNotPresent(Object registry) { + MetricRegistry metricRegistry = (MetricRegistry) registry; + assertThat(metricRegistry.getMetrics()).isEmpty(); + } + @Override protected void assertNodeMetricsEvicted(CqlSession session, Node node) { InternalDriverContext context = (InternalDriverContext) session.getContext(); diff --git a/metrics/micrometer/src/main/java/com/datastax/oss/driver/internal/metrics/micrometer/MicrometerMetricUpdater.java b/metrics/micrometer/src/main/java/com/datastax/oss/driver/internal/metrics/micrometer/MicrometerMetricUpdater.java index 7a4a27991e3..b9507c8b7cf 100644 --- a/metrics/micrometer/src/main/java/com/datastax/oss/driver/internal/metrics/micrometer/MicrometerMetricUpdater.java +++ b/metrics/micrometer/src/main/java/com/datastax/oss/driver/internal/metrics/micrometer/MicrometerMetricUpdater.java @@ -83,7 +83,7 @@ public void updateTimer( } @Override - protected void clearMetrics() { + public void clearMetrics() { for (Meter metric : metrics.values()) { registry.remove(metric); } diff --git a/metrics/microprofile/src/main/java/com/datastax/oss/driver/internal/metrics/microprofile/MicroProfileMetricUpdater.java b/metrics/microprofile/src/main/java/com/datastax/oss/driver/internal/metrics/microprofile/MicroProfileMetricUpdater.java index a46e82ee624..df44fd69c51 100644 --- a/metrics/microprofile/src/main/java/com/datastax/oss/driver/internal/metrics/microprofile/MicroProfileMetricUpdater.java +++ b/metrics/microprofile/src/main/java/com/datastax/oss/driver/internal/metrics/microprofile/MicroProfileMetricUpdater.java @@ -83,7 +83,7 @@ public void updateTimer( } @Override - protected void clearMetrics() { + public void clearMetrics() { for (MetricT metric : metrics.keySet()) { MetricId id = getMetricId(metric); Tag[] tags = MicroProfileTags.toMicroProfileTags(id.getTags()); From b3d1b4a62680129d9f430795acd59c98600cc24f Mon Sep 17 00:00:00 2001 From: Bret McGuire Date: Mon, 19 Feb 2024 23:07:18 -0600 Subject: [PATCH 14/24] Changelog updates to reflect work that went out in 4.18.0 Patch by Bret McGuire; reviewed by Bret McGuire, Alexandre Dutra for PR 1914 --- changelog/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog/README.md b/changelog/README.md index 2d859526586..a4efd9c8594 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -21,6 +21,16 @@ under the License. +### 4.18.0 + +- [improvement] PR 1689: Add support for publishing percentile time series for the histogram metrics (nparaddi-walmart) +- [improvement] JAVA-3104: Do not eagerly pre-allocate array when deserializing CqlVector +- [improvement] JAVA-3111: upgrade jackson-databind to 2.13.4.2 to address gradle dependency issue +- [improvement] PR 1617: Improve ByteBufPrimitiveCodec readBytes (chibenwa) +- [improvement] JAVA-3095: Fix CREATE keyword in vector search example in upgrade guide +- [improvement] JAVA-3100: Update jackson-databind to 2.13.4.1 and jackson-jaxrs-json-provider to 2.13.4 to address recent CVEs +- [improvement] JAVA-3089: Forbid wildcard imports + ### 4.17.0 - [improvement] JAVA-3070: Make CqlVector and CqlDuration serializable From d696153fd0a6a57ebd8d8e07d4a1227a73e49f40 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 17 Apr 2024 01:34:40 -0500 Subject: [PATCH 15/24] Fixes to get past code formatting issues patch by Bret McGuire; reviewed by Bret McGuire for PR 1928 --- .../core/metadata/MetadataManager.java | 46 +++++++++---------- .../core/metadata/MetadataManagerTest.java | 12 +++-- 2 files changed, 31 insertions(+), 27 deletions(-) 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 index 1c53dd49e62..09070525f87 100644 --- 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 @@ -449,29 +449,29 @@ private void startSchemaRequest(CompletableFuture refreshFu } else { try { schemaQueriesFactory - .newInstance() - .execute() - .thenApplyAsync(this::parseAndApplySchemaRows, adminExecutor) - .whenComplete( - (newMetadata, metadataError) -> { - if (metadataError != null) { - refreshFuture.completeExceptionally(metadataError); - } else { - refreshFuture.complete( - new RefreshSchemaResult(newMetadata, schemaInAgreement)); - } - - firstSchemaRefreshFuture.complete(null); - - currentSchemaRefresh = null; - // If another refresh was enqueued during this one, run it now - if (queuedSchemaRefresh != null) { - CompletableFuture tmp = - this.queuedSchemaRefresh; - this.queuedSchemaRefresh = null; - startSchemaRequest(tmp); - } - }); + .newInstance() + .execute() + .thenApplyAsync(this::parseAndApplySchemaRows, adminExecutor) + .whenComplete( + (newMetadata, metadataError) -> { + if (metadataError != null) { + refreshFuture.completeExceptionally(metadataError); + } else { + refreshFuture.complete( + new RefreshSchemaResult(newMetadata, schemaInAgreement)); + } + + firstSchemaRefreshFuture.complete(null); + + currentSchemaRefresh = null; + // If another refresh was enqueued during this one, run it now + if (queuedSchemaRefresh != null) { + CompletableFuture tmp = + this.queuedSchemaRefresh; + this.queuedSchemaRefresh = null; + startSchemaRequest(tmp); + } + }); } catch (Throwable t) { LOG.debug("[{}] Exception getting new metadata", logPrefix, t); refreshFuture.completeExceptionally(t); 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 index 375209d9fcf..f9a909400f9 100644 --- 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 @@ -295,14 +295,18 @@ public void refreshSchema_should_work() { // Given IllegalStateException expectedException = new IllegalStateException("Error we're testing"); when(schemaQueriesFactory.newInstance()).thenThrow(expectedException); - when(topologyMonitor.refreshNodeList()).thenReturn(CompletableFuture.completedFuture(ImmutableList.of(mock(NodeInfo.class)))); - when(topologyMonitor.checkSchemaAgreement()).thenReturn(CompletableFuture.completedFuture(Boolean.TRUE)); - when(controlConnection.init(anyBoolean(), anyBoolean(), anyBoolean())).thenReturn(CompletableFuture.completedFuture(null)); + when(topologyMonitor.refreshNodeList()) + .thenReturn(CompletableFuture.completedFuture(ImmutableList.of(mock(NodeInfo.class)))); + when(topologyMonitor.checkSchemaAgreement()) + .thenReturn(CompletableFuture.completedFuture(Boolean.TRUE)); + when(controlConnection.init(anyBoolean(), anyBoolean(), anyBoolean())) + .thenReturn(CompletableFuture.completedFuture(null)); metadataManager.refreshNodes(); // required internal state setup for this waitForPendingAdminTasks(() -> metadataManager.refreshes.size() == 1); // sanity check // When - CompletionStage result = metadataManager.refreshSchema("foo", true, true); + CompletionStage result = + metadataManager.refreshSchema("foo", true, true); // Then waitForPendingAdminTasks(() -> result.toCompletableFuture().isDone()); From b4bd25c11820375a6447b1f111ea3a278636d7ac Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 23 Apr 2024 00:38:48 -0500 Subject: [PATCH 16/24] Initial fix to unit tests patch by Bret McGuire; reviewed by Bret McGuire for PR 1930 --- .../driver/internal/core/session/DefaultSession.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 index 17516cef390..be375cd58e3 100644 --- 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 @@ -46,6 +46,7 @@ import com.datastax.oss.driver.internal.core.metadata.MetadataManager.RefreshSchemaResult; 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.NodeMetricUpdater; 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; @@ -571,10 +572,11 @@ private void close() { // clear metrics to prevent memory leak for (Node n : metadataManager.getMetadata().getNodes().values()) { - ((DefaultNode) n).getMetricUpdater().clearMetrics(); + NodeMetricUpdater updater = ((DefaultNode) n).getMetricUpdater(); + if (updater != null) updater.clearMetrics(); } - DefaultSession.this.metricUpdater.clearMetrics(); + if (metricUpdater != null) metricUpdater.clearMetrics(); List> childrenCloseStages = new ArrayList<>(); for (AsyncAutoCloseable closeable : internalComponentsToClose()) { @@ -597,10 +599,11 @@ private void forceClose() { // clear metrics to prevent memory leak for (Node n : metadataManager.getMetadata().getNodes().values()) { - ((DefaultNode) n).getMetricUpdater().clearMetrics(); + NodeMetricUpdater updater = ((DefaultNode) n).getMetricUpdater(); + if (updater != null) updater.clearMetrics(); } - DefaultSession.this.metricUpdater.clearMetrics(); + if (metricUpdater != null) metricUpdater.clearMetrics(); if (closeWasCalled) { // onChildrenClosed has already been scheduled From 22126641fedeefc96b949d47eb3b4f04a109782d Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Fri, 29 Mar 2024 00:46:46 -0500 Subject: [PATCH 17/24] CASSANDRA-19292: Enable Jenkins to test against Cassandra 4.1.x patch by Bret McGuire; reviewed by Bret McGuire, Alexandre Dutra for CASSANDRA-19292 --- Jenkinsfile | 20 +++- .../driver/api/testinfra/ccm/CcmBridge.java | 103 ++++++++++++++++-- 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8d2b74c5b08..0bfa4ca7f4a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -256,8 +256,10 @@ pipeline { 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Ⓡ + '3.11', // Previous Apache CassandraⓇ + '4.0', // Previous Apache CassandraⓇ + '4.1', // Current Apache CassandraⓇ + '5.0', // Development Apache CassandraⓇ 'dse-4.8.16', // Previous EOSL DataStax Enterprise 'dse-5.0.15', // Long Term Support DataStax Enterprise 'dse-5.1.35', // Legacy DataStax Enterprise @@ -291,7 +293,11 @@ pipeline { 4.0 - Apache Cassandra® v4.x (CURRENTLY UNDER DEVELOPMENT) + Apache Cassandra® v4.0.x + + + 4.1 + Apache Cassandra® v4.1.x dse-4.8.16 @@ -445,7 +451,7 @@ pipeline { axis { name 'SERVER_VERSION' values '3.11', // Latest stable Apache CassandraⓇ - '4.0', // Development Apache CassandraⓇ + '4.1', // Development Apache CassandraⓇ 'dse-6.8.30' // Current DataStax Enterprise } axis { @@ -554,8 +560,10 @@ pipeline { name 'SERVER_VERSION' values '2.1', // Legacy Apache CassandraⓇ '3.0', // Previous Apache CassandraⓇ - '3.11', // Current Apache CassandraⓇ - '4.0', // Development Apache CassandraⓇ + '3.11', // Previous Apache CassandraⓇ + '4.0', // Previous Apache CassandraⓇ + '4.1', // Current Apache CassandraⓇ + '5.0', // Development Apache CassandraⓇ 'dse-4.8.16', // Previous EOSL DataStax Enterprise 'dse-5.0.15', // Last EOSL DataStax Enterprise 'dse-5.1.35', // Legacy DataStax Enterprise 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 index b14d02455f8..05d1d129b16 100644 --- 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 @@ -327,23 +327,39 @@ public void create() { // Collect all cassandraConfiguration (and others) into a single "ccm updateconf" call. // Works around the behavior introduced in https://github.com/scylladb/scylla-ccm/pull/410 StringBuilder updateConfArguments = new StringBuilder(); + Version cassandraVersion = getCassandraVersion(); for (Map.Entry conf : cassandraConfiguration.entrySet()) { - updateConfArguments.append(conf.getKey()).append(':').append(conf.getValue()).append(' '); + String originalKey = conf.getKey(); + Object originalValue = conf.getValue(); + updateConfArguments.append( + String.join( + ":", + getConfigKey(originalKey, originalValue, cassandraVersion), + getConfigValue(originalKey, originalValue, cassandraVersion)) + + " "); } - if (getCassandraVersion().compareTo(Version.V2_2_0) >= 0 && !SCYLLA_ENABLEMENT) { - // @IntegrationTestDisabledScyllaJVMArgs @IntegrationTestDisabledScyllaUDF - if (getCassandraVersion().compareTo(Version.V4_1_0) >= 0) { - updateConfArguments.append("user_defined_functions_enabled:true").append(' '); - } else { - updateConfArguments.append("enable_user_defined_functions:true").append(' '); + if (!SCYLLA_ENABLEMENT) { + // If we're dealing with anything more recent than 2.2 explicitly enable UDF... but run it + // through our conversion process to make + // sure more recent versions don't have a problem. + if (getCassandraVersion().compareTo(Version.V2_2_0) >= 0) { + String originalKey = "enable_user_defined_functions"; + Object originalValue = "true"; + updateConfArguments.append( + String.join( + ":", + getConfigKey(originalKey, originalValue, cassandraVersion), + getConfigValue(originalKey, originalValue, cassandraVersion))); } } if (updateConfArguments.length() > 0) { execute("updateconf", updateConfArguments.toString()); } + + // Note that we aren't performing any substitution on DSE key/value props (at least for now) if (DSE_ENABLEMENT) { for (Map.Entry conf : dseConfiguration.entrySet()) { execute("updatedseconf", String.format("%s:%s", conf.getKey(), conf.getValue())); @@ -517,6 +533,79 @@ private static File createTempStore(String storePath) { return f; } + /** + * Get the current JVM major version (1.8.0_372 -> 8, 11.0.19 -> 11) + * + * @return major version of current JVM + */ + private static int getCurrentJvmMajorVersion() { + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dot = version.indexOf("."); + if (dot != -1) { + version = version.substring(0, dot); + } + } + return Integer.parseInt(version); + } + + @SuppressWarnings("UnusedMethod") + private Optional overrideJvmVersionForDseWorkloads() { + if (getCurrentJvmMajorVersion() <= 8) { + return Optional.empty(); + } + + if (!DSE_ENABLEMENT || !getDseVersion().isPresent()) { + return Optional.empty(); + } + + if (getDseVersion().get().compareTo(Version.parse("6.8.19")) < 0) { + return Optional.empty(); + } + + if (dseWorkloads.contains("graph")) { + return Optional.of(8); + } + + return Optional.empty(); + } + + private static String IN_MS_STR = "_in_ms"; + private static int IN_MS_STR_LENGTH = IN_MS_STR.length(); + private static String ENABLE_STR = "enable_"; + private static int ENABLE_STR_LENGTH = ENABLE_STR.length(); + private static String IN_KB_STR = "_in_kb"; + private static int IN_KB_STR_LENGTH = IN_KB_STR.length(); + + @SuppressWarnings("unused") + private String getConfigKey(String originalKey, Object originalValue, Version cassandraVersion) { + + // At least for now we won't support substitutions on nested keys. This requires an extra + // traversal of the string + // but we'll live with that for now + if (originalKey.contains(".")) return originalKey; + if (cassandraVersion.compareTo(Version.V4_1_0) < 0) return originalKey; + if (originalKey.endsWith(IN_MS_STR)) + return originalKey.substring(0, originalKey.length() - IN_MS_STR_LENGTH); + if (originalKey.startsWith(ENABLE_STR)) + return originalKey.substring(ENABLE_STR_LENGTH) + "_enabled"; + if (originalKey.endsWith(IN_KB_STR)) + return originalKey.substring(0, originalKey.length() - IN_KB_STR_LENGTH); + return originalKey; + } + + private String getConfigValue( + String originalKey, Object originalValue, Version cassandraVersion) { + + String originalValueStr = originalValue.toString(); + if (cassandraVersion.compareTo(Version.V4_1_0) < 0) return originalValueStr; + if (originalKey.endsWith(IN_MS_STR)) return originalValueStr + "ms"; + if (originalKey.endsWith(IN_KB_STR)) return originalValueStr + "KiB"; + return originalValueStr; + } + public static Builder builder() { return new Builder(); } From c2bc2f447de3d7a9cb817053fd224fc1d74b0677 Mon Sep 17 00:00:00 2001 From: Nitin Chhabra Date: Thu, 30 Nov 2023 12:38:23 -0800 Subject: [PATCH 18/24] JAVA-3142: Ability to specify ordering of remote local dc's via new configuration for graceful automatic failovers patch by Nitin Chhabra; reviewed by Alexandre Dutra, Andy Tolbert, and Bret McGuire for JAVA-3142 --- .../api/core/config/DefaultDriverOption.java | 8 +- .../driver/api/core/config/OptionsMap.java | 2 + .../api/core/config/TypedDriverOption.java | 10 + .../BasicLoadBalancingPolicy.java | 82 ++++++-- core/src/main/resources/reference.conf | 5 + ...BalancingPolicyPreferredRemoteDcsTest.java | 184 ++++++++++++++++++ 6 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicyPreferredRemoteDcsTest.java 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 index 9812eab4cea..224ed3b11bd 100644 --- 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 @@ -988,7 +988,13 @@ public enum DefaultDriverOption implements DriverOption { *

Value-type: {@link java.time.Duration} */ SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"), - ; + /** + * Ordered preference list of remote dcs optionally supplied for automatic failover. + * + *

Value type: {@link java.util.List List}<{@link String}> + */ + LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS( + "advanced.load-balancing-policy.dc-failover.preferred-remote-dcs"); private final String path; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java index 8906e1dd349..98faf3e590c 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java @@ -381,6 +381,8 @@ protected static void fillWithDriverDefaults(OptionsMap map) { map.put(TypedDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, 0); map.put(TypedDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS, false); map.put(TypedDriverOption.METRICS_GENERATE_AGGREGABLE_HISTOGRAMS, true); + map.put( + TypedDriverOption.LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS, ImmutableList.of("")); } @Immutable diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java index 8f834fd96bd..943c8f9fb75 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java @@ -895,6 +895,16 @@ public String toString() { DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS, GenericType.BOOLEAN); + /** + * Ordered preference list of remote dcs optionally supplied for automatic failover and included + * in query plan. This feature is enabled only when max-nodes-per-remote-dc is greater than 0. + */ + public static final TypedDriverOption> + LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS = + new TypedDriverOption<>( + DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS, + GenericType.listOf(String.class)); + private static Iterable> introspectBuiltInValues() { try { ImmutableList.Builder> result = ImmutableList.builder(); diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicy.java index 996c967b7f8..a65ab769096 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicy.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicy.java @@ -56,10 +56,14 @@ import com.datastax.oss.driver.internal.core.util.collection.QueryPlan; import com.datastax.oss.driver.internal.core.util.collection.SimpleQueryPlan; import com.datastax.oss.driver.shaded.guava.common.base.Predicates; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +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.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -129,6 +133,7 @@ public class BasicLoadBalancingPolicy implements LoadBalancingPolicy { private volatile String localDc; private volatile String localRack; private volatile NodeSet liveNodes; + private final LinkedHashSet preferredRemoteDcs; public BasicLoadBalancingPolicy(@NonNull DriverContext context, @NonNull String profileName) { this.context = (InternalDriverContext) context; @@ -143,6 +148,11 @@ public BasicLoadBalancingPolicy(@NonNull DriverContext context, @NonNull String this.context .getConsistencyLevelRegistry() .nameToLevel(profile.getString(DefaultDriverOption.REQUEST_CONSISTENCY)); + + preferredRemoteDcs = + new LinkedHashSet<>( + profile.getStringList( + DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS)); } /** @@ -370,27 +380,59 @@ protected Queue maybeAddDcFailover(@Nullable Request request, @NonNull Que return local; } } - QueryPlan remote = - new LazyQueryPlan() { - - @Override - protected Object[] computeNodes() { - Object[] remoteNodes = - liveNodes.dcs().stream() - .filter(Predicates.not(Predicates.equalTo(localDc))) - .flatMap(dc -> liveNodes.dc(dc).stream().limit(maxNodesPerRemoteDc)) - .toArray(); - - int remoteNodesLength = remoteNodes.length; - if (remoteNodesLength == 0) { - return EMPTY_NODES; - } - shuffleHead(remoteNodes, remoteNodesLength); - return remoteNodes; - } - }; + if (preferredRemoteDcs.isEmpty()) { + return new CompositeQueryPlan(local, buildRemoteQueryPlanAll()); + } + return new CompositeQueryPlan(local, buildRemoteQueryPlanPreferred()); + } + + private QueryPlan buildRemoteQueryPlanAll() { + + return new LazyQueryPlan() { + @Override + protected Object[] computeNodes() { + + Object[] remoteNodes = + liveNodes.dcs().stream() + .filter(Predicates.not(Predicates.equalTo(localDc))) + .flatMap(dc -> liveNodes.dc(dc).stream().limit(maxNodesPerRemoteDc)) + .toArray(); + if (remoteNodes.length == 0) { + return EMPTY_NODES; + } + shuffleHead(remoteNodes, remoteNodes.length); + return remoteNodes; + } + }; + } - return new CompositeQueryPlan(local, remote); + private QueryPlan buildRemoteQueryPlanPreferred() { + + Set dcs = liveNodes.dcs(); + List orderedDcs = Lists.newArrayListWithCapacity(dcs.size()); + orderedDcs.addAll(preferredRemoteDcs); + orderedDcs.addAll(Sets.difference(dcs, preferredRemoteDcs)); + + QueryPlan[] queryPlans = + orderedDcs.stream() + .filter(Predicates.not(Predicates.equalTo(localDc))) + .map( + (dc) -> { + return new LazyQueryPlan() { + @Override + protected Object[] computeNodes() { + Object[] rv = liveNodes.dc(dc).stream().limit(maxNodesPerRemoteDc).toArray(); + if (rv.length == 0) { + return EMPTY_NODES; + } + shuffleHead(rv, rv.length); + return rv; + } + }; + }) + .toArray(QueryPlan[]::new); + + return new CompositeQueryPlan(queryPlans); } /** Exposed as a protected method so that it can be accessed by tests */ diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index d1ac22e553b..7a56a18e9f1 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -574,6 +574,11 @@ datastax-java-driver { # Modifiable at runtime: no # Overridable in a profile: yes allow-for-local-consistency-levels = false + # Ordered preference list of remote dc's (in order) optionally supplied for automatic failover. While building a query plan, the driver uses the DC's supplied in order together with max-nodes-per-remote-dc + # Required: no + # Modifiable at runtime: no + # Overridable in a profile: no + preferred-remote-dcs = [""] } } diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicyPreferredRemoteDcsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicyPreferredRemoteDcsTest.java new file mode 100644 index 00000000000..cefdfd31189 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/BasicLoadBalancingPolicyPreferredRemoteDcsTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 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.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.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.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.util.Map; +import java.util.UUID; +import org.junit.Test; +import org.mockito.Mock; + +public class BasicLoadBalancingPolicyPreferredRemoteDcsTest + extends BasicLoadBalancingPolicyDcFailoverTest { + @Mock protected DefaultNode node10; + @Mock protected DefaultNode node11; + @Mock protected DefaultNode node12; + @Mock protected DefaultNode node13; + @Mock protected DefaultNode node14; + + @Override + @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, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node3, node2, node4, node5, node1, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node3, node4, node5, node1, node2, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node3, node5, node1, node2, node4, node9, node10, node6, node7, node12, node13); + + // Should not shuffle replicas since there is only one + verify(policy, never()).shuffleHead(any(), eq(1)); + // But should shuffle remote nodes + verify(policy, times(12)).shuffleHead(any(), eq(2)); + } + + @Override + @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(node1, node2, node3, node6, node9)); + + // node 6 and 9 being in a remote DC, they don't get a boost for being a replica + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node1, node2, node3, node4, node5, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node1, node2, node3, node5, node4, node9, node10, node6, node7, node12, node13); + + // should shuffle replicas + verify(policy, times(2)).shuffleHead(any(), eq(3)); + // should shuffle remote nodes + verify(policy, times(6)).shuffleHead(any(), eq(2)); + // No power of two choices with only two replicas + verify(session, never()).getPools(); + } + + @Override + protected void assertRoundRobinQueryPlans() { + for (int i = 0; i < 3; i++) { + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node1, node2, node3, node4, node5, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node2, node3, node4, node5, node1, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node3, node4, node5, node1, node2, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node4, node5, node1, node2, node3, node9, node10, node6, node7, node12, node13); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly( + node5, node1, node2, node3, node4, node9, node10, node6, node7, node12, node13); + } + + verify(policy, atLeast(15)).shuffleHead(any(), eq(2)); + } + + @Override + protected BasicLoadBalancingPolicy createAndInitPolicy() { + when(node4.getDatacenter()).thenReturn("dc1"); + when(node5.getDatacenter()).thenReturn("dc1"); + when(node6.getDatacenter()).thenReturn("dc2"); + when(node7.getDatacenter()).thenReturn("dc2"); + when(node8.getDatacenter()).thenReturn("dc2"); + when(node9.getDatacenter()).thenReturn("dc3"); + when(node10.getDatacenter()).thenReturn("dc3"); + when(node11.getDatacenter()).thenReturn("dc3"); + when(node12.getDatacenter()).thenReturn("dc4"); + when(node13.getDatacenter()).thenReturn("dc4"); + when(node14.getDatacenter()).thenReturn("dc4"); + + // Accept 2 nodes per remote DC + when(defaultProfile.getInt( + DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC)) + .thenReturn(2); + when(defaultProfile.getBoolean( + DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS)) + .thenReturn(false); + + when(defaultProfile.getStringList( + DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_PREFERRED_REMOTE_DCS)) + .thenReturn(ImmutableList.of("dc3", "dc2")); + + // Use a subclass to disable shuffling, we just spy to make sure that the shuffling method was + // called (makes tests easier) + BasicLoadBalancingPolicy policy = + spy( + new BasicLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME) { + @Override + protected void shuffleHead(Object[] currentNodes, int headLength) { + // nothing (keep in same order) + } + }); + Map nodes = + ImmutableMap.builder() + .put(UUID.randomUUID(), node1) + .put(UUID.randomUUID(), node2) + .put(UUID.randomUUID(), node3) + .put(UUID.randomUUID(), node4) + .put(UUID.randomUUID(), node5) + .put(UUID.randomUUID(), node6) + .put(UUID.randomUUID(), node7) + .put(UUID.randomUUID(), node8) + .put(UUID.randomUUID(), node9) + .put(UUID.randomUUID(), node10) + .put(UUID.randomUUID(), node11) + .put(UUID.randomUUID(), node12) + .put(UUID.randomUUID(), node13) + .put(UUID.randomUUID(), node14) + .build(); + policy.init(nodes, distanceReporter); + assertThat(policy.getLiveNodes().dc("dc1")).containsExactly(node1, node2, node3, node4, node5); + assertThat(policy.getLiveNodes().dc("dc2")).containsExactly(node6, node7); // only 2 allowed + assertThat(policy.getLiveNodes().dc("dc3")).containsExactly(node9, node10); // only 2 allowed + assertThat(policy.getLiveNodes().dc("dc4")).containsExactly(node12, node13); // only 2 allowed + return policy; + } +} From 48f1e8668c5a510d147582f57f463a253960ddaa Mon Sep 17 00:00:00 2001 From: janehe Date: Wed, 17 Apr 2024 14:59:59 -0700 Subject: [PATCH 19/24] CASSANDRA-19568: Use Jabba to specify Java 1.8 for building the driver patch by Jane He and Bret McGuire; reviewed by Bret McGuire for CASSANDRA-19568 --- Jenkinsfile | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0bfa4ca7f4a..69ee0a294c0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -98,14 +98,16 @@ ENVIRONMENT_EOF } def buildDriver(jabbaVersion) { - withEnv(["BUILD_JABBA_VERSION=${jabbaVersion}"]) { - sh label: 'Build driver', script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba use ${BUILD_JABBA_VERSION} + def buildDriverScript = '''#!/bin/bash -le - mvn -B -V install -DskipTests -Dmaven.javadoc.skip=true - ''' - } + . ${JABBA_SHELL} + jabba use '''+jabbaVersion+''' + + echo "Building with Java version '''+jabbaVersion+''' + + mvn -B -V install -DskipTests -Dmaven.javadoc.skip=true + ''' + sh label: 'Build driver', script: buildDriverScript } def executeTests() { @@ -484,7 +486,7 @@ pipeline { } stage('Build-Driver') { steps { - buildDriver('default') + buildDriver('1.8') } } stage('Execute-Tests') { @@ -600,8 +602,7 @@ pipeline { } stage('Build-Driver') { steps { - // Jabba default should be a JDK8 for now - buildDriver('default') + buildDriver('1.8') } } stage('Execute-Tests') { From 959367d0175d44c26621f72bc0efe68728d7a806 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 8 May 2024 21:37:02 -0500 Subject: [PATCH 20/24] ninja-fix CASSANDRA-19568: fixing mangled Groovy --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 69ee0a294c0..d38b7c63849 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -103,7 +103,7 @@ def buildDriver(jabbaVersion) { . ${JABBA_SHELL} jabba use '''+jabbaVersion+''' - echo "Building with Java version '''+jabbaVersion+''' + echo "Building with Java version '''+jabbaVersion+'''" mvn -B -V install -DskipTests -Dmaven.javadoc.skip=true ''' From ff3fd5f1d5aa6c8ad88b4d7d6dded9cae4418940 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 16 May 2024 20:55:25 -0500 Subject: [PATCH 21/24] ninja-fix updating repo for releases --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f91fba3ad59..eed0bb0d5aa 100644 --- a/pom.xml +++ b/pom.xml @@ -713,9 +713,9 @@ true ossrh - https://oss.sonatype.org/ - true - + https://repository.apache.org/ + false + true From 5d667a7da6fe4f091af95bb3812da905a6cc639a Mon Sep 17 00:00:00 2001 From: Nitin Chhabra Date: Wed, 8 May 2024 16:54:43 -0700 Subject: [PATCH 22/24] JAVA-3142: Improving the documentation for remote local dc's feature patch by Nitin Chhabra; reviewed by Bret McGuire for JAVA-3142 --- core/src/main/resources/reference.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 7a56a18e9f1..7b1c43f8bea 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -574,7 +574,9 @@ datastax-java-driver { # Modifiable at runtime: no # Overridable in a profile: yes allow-for-local-consistency-levels = false + # Ordered preference list of remote dc's (in order) optionally supplied for automatic failover. While building a query plan, the driver uses the DC's supplied in order together with max-nodes-per-remote-dc + # Users are not required to specify all DCs, when listing preferences via this config # Required: no # Modifiable at runtime: no # Overridable in a profile: no From 7a4cab210c5ffad8a7900e246b032a189ffe6dc2 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Fri, 17 May 2024 12:27:53 -0500 Subject: [PATCH 23/24] ninja-fix changlog updates for 4.18.1 --- changelog/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog/README.md b/changelog/README.md index a4efd9c8594..02c88680e49 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -21,6 +21,16 @@ under the License. +### 4.18.1 + +- [improvement] JAVA-3142: Ability to specify ordering of remote local dc's via new configuration for graceful automatic failovers +- [bug] CASSANDRA-19457: Object reference in Micrometer metrics prevent GC from reclaiming Session instances +- [improvement] CASSANDRA-19468: Don't swallow exception during metadata refresh +- [bug] CASSANDRA-19333: Fix data corruption in VectorCodec when using heap buffers +- [improvement] CASSANDRA-19290: Replace uses of AttributeKey.newInstance +- [improvement] CASSANDRA-19352: Support native_transport_(address|port) + native_transport_port_ssl for DSE 6.8 (4.x edition) +- [improvement] CASSANDRA-19180: Support reloading keystore in cassandra-java-driver + ### 4.18.0 - [improvement] PR 1689: Add support for publishing percentile time series for the histogram metrics (nparaddi-walmart) From ef43e74e41119188ff383cd0960bf18353509b7c Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Mon, 20 May 2024 09:57:23 -0500 Subject: [PATCH 24/24] [maven-release-plugin] prepare release 4.18.1 --- bom/pom.xml | 18 +++++++++--------- core-shaded/pom.xml | 2 +- core/pom.xml | 2 +- distribution-source/pom.xml | 2 +- distribution-tests/pom.xml | 2 +- distribution/pom.xml | 2 +- examples/pom.xml | 2 +- integration-tests/pom.xml | 2 +- mapper-processor/pom.xml | 2 +- mapper-runtime/pom.xml | 2 +- metrics/micrometer/pom.xml | 2 +- metrics/microprofile/pom.xml | 2 +- osgi-tests/pom.xml | 2 +- pom.xml | 4 ++-- query-builder/pom.xml | 2 +- test-infra/pom.xml | 2 +- 16 files changed, 25 insertions(+), 25 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 0fbb66dcc4e..c6288d601e2 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-bom pom @@ -38,42 +38,42 @@ com.scylladb java-driver-core - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-core-shaded - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-mapper-processor - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-mapper-runtime - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-query-builder - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-test-infra - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-metrics-micrometer - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.scylladb java-driver-metrics-microprofile - 4.18.1.0-SNAPSHOT + 4.18.1.0 com.datastax.oss diff --git a/core-shaded/pom.xml b/core-shaded/pom.xml index c8f707175ee..20789e02458 100644 --- a/core-shaded/pom.xml +++ b/core-shaded/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-core-shaded Java driver for Scylla and Apache Cassandra(R) - core with shaded deps diff --git a/core/pom.xml b/core/pom.xml index fd86f4431dc..228730e1142 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-core bundle diff --git a/distribution-source/pom.xml b/distribution-source/pom.xml index 29ac63e98a5..f68ca1b4564 100644 --- a/distribution-source/pom.xml +++ b/distribution-source/pom.xml @@ -23,7 +23,7 @@ org.apache.cassandra java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-distribution-source pom diff --git a/distribution-tests/pom.xml b/distribution-tests/pom.xml index 504ef24ebdb..1c0dd09a1b3 100644 --- a/distribution-tests/pom.xml +++ b/distribution-tests/pom.xml @@ -23,7 +23,7 @@ org.apache.cassandra java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-distribution-tests Apache Cassandra Java Driver - distribution tests diff --git a/distribution/pom.xml b/distribution/pom.xml index 06f54854ef0..ee54fa44e77 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-distribution diff --git a/examples/pom.xml b/examples/pom.xml index 5f55d3e04cf..92ed8c723d4 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -28,7 +28,7 @@ java-driver-parent com.scylladb - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-examples Java driver for Scylla and Apache Cassandra(R) - examples. diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 627bb8f66f2..8e3bb7ebad6 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-integration-tests jar diff --git a/mapper-processor/pom.xml b/mapper-processor/pom.xml index 96fcb636d44..99ae9ec2fee 100644 --- a/mapper-processor/pom.xml +++ b/mapper-processor/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-mapper-processor Java driver for Scylla and Apache Cassandra(R) - object mapper processor diff --git a/mapper-runtime/pom.xml b/mapper-runtime/pom.xml index 13f25db2230..a8ee2faa8df 100644 --- a/mapper-runtime/pom.xml +++ b/mapper-runtime/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-mapper-runtime bundle diff --git a/metrics/micrometer/pom.xml b/metrics/micrometer/pom.xml index 6d0110e9b55..a04fe3e2553 100644 --- a/metrics/micrometer/pom.xml +++ b/metrics/micrometer/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 ../../ java-driver-metrics-micrometer diff --git a/metrics/microprofile/pom.xml b/metrics/microprofile/pom.xml index b9a07939a73..ff68bc6fbbd 100644 --- a/metrics/microprofile/pom.xml +++ b/metrics/microprofile/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 ../../ java-driver-metrics-microprofile diff --git a/osgi-tests/pom.xml b/osgi-tests/pom.xml index 37463fb547f..36ea8714c12 100644 --- a/osgi-tests/pom.xml +++ b/osgi-tests/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-osgi-tests jar diff --git a/pom.xml b/pom.xml index eed0bb0d5aa..ae7eb9a1e3f 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 4.0.0 com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 pom Java Driver for Scylla and Apache Cassandra A driver for Scylla and Apache Cassandra(R) 2.1+ that works exclusively with the Cassandra Query Language version 3 (CQL3) and Cassandra's native protocol versions 3 and above. @@ -1033,7 +1033,7 @@ height="0" width="0" style="display:none;visibility:hidden"> scm:git:https://github.com/scylladb/java-driver scm:git:https://github.com/scylladb/java-driver https://github.com/scylladb/java-driver - HEAD + 4.18.1.0 diff --git a/query-builder/pom.xml b/query-builder/pom.xml index d718fce2d23..1ad8ce86175 100644 --- a/query-builder/pom.xml +++ b/query-builder/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-query-builder bundle diff --git a/test-infra/pom.xml b/test-infra/pom.xml index 8c775ce4a93..e14e2010af1 100644 --- a/test-infra/pom.xml +++ b/test-infra/pom.xml @@ -28,7 +28,7 @@ com.scylladb java-driver-parent - 4.18.1.0-SNAPSHOT + 4.18.1.0 java-driver-test-infra bundle