This is an automated email from the ASF dual-hosted git repository.
jeetkundoug pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-in-jvm-dtest-api.git
The following commit(s) were added to refs/heads/trunk by this push:
new fab3eb2 CASSANDRA-20884 - Move JMX classes to the in-jvm-dtest API
project
fab3eb2 is described below
commit fab3eb2c23a9f9a33a46cd6ad865dff5b450be6e
Author: Doug Rohrer <[email protected]>
AuthorDate: Fri Aug 29 16:54:49 2025 -0400
CASSANDRA-20884 - Move JMX classes to the in-jvm-dtest API project
This commit brings several important JMX-related test classes over from
the Cassandra codebase to the in-jvm-dtest library for two reasons:
1) They are duplicated in every Cassandra version from 4.0 onwards, and
don't need to be.
2) More imporantly, using these classes outside of the Cassandra project
(in sidecar and analytics) requires that those projects also have
access to these classes, and we've worked around this with some fairly
ugly hacks to date. Moving these will eliminate the need for the hacks.
Some additional, smaller changes include:
- Updates to the AbstractBuilder class to bring the `B self()` method over.
- Add code to enable dynamic port allocation
- Throw a more intelligent exception when we can't find a dtest-jar
Co-authored-by: Francisco Guerrero <[email protected]>
---
pom.xml | 2 +-
.../apache/cassandra/distributed/api/ICluster.java | 13 +-
.../distributed/shared/AbstractBuilder.java | 79 +++++++----
.../cassandra/distributed/shared/Versions.java | 24 +++-
.../jmx/CollectingRMIServerSocketFactoryImpl.java | 87 ++++++++++++
.../CollectingSslRMIServerSocketFactoryImpl.java | 149 +++++++++++++++++++++
.../shared/jmx/RMIClientSocketFactoryImpl.java | 82 ++++++++++++
.../jmx/RMICloseableClientSocketFactory.java | 29 ++++
.../jmx/RMICloseableServerSocketFactory.java | 29 ++++
.../shared/jmx/RMISslClientSocketFactoryImpl.java | 131 ++++++++++++++++++
.../cassandra/distributed/shared/VersionsTest.java | 17 ++-
11 files changed, 607 insertions(+), 35 deletions(-)
diff --git a/pom.xml b/pom.xml
index 9c9c6fc..6ed53b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -164,7 +164,7 @@
<connection>scm:git:https://gitbox.apache.org/repos/asf/cassandra-in-jvm-dtest-api.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/cassandra-in-jvm-dtest-api.git</developerConnection>
<url>https://gitbox.apache.org/repos/asf/cassandra-in-jvm-dtest-api.git</url>
- <tag>0.0.16</tag>
+ <tag>0.0.18</tag>
</scm>
</project>
diff --git a/src/main/java/org/apache/cassandra/distributed/api/ICluster.java
b/src/main/java/org/apache/cassandra/distributed/api/ICluster.java
index f5ff75d..8b9f68d 100644
--- a/src/main/java/org/apache/cassandra/distributed/api/ICluster.java
+++ b/src/main/java/org/apache/cassandra/distributed/api/ICluster.java
@@ -47,6 +47,8 @@ public interface ICluster<I extends IInstance> extends
AutoCloseable, Iterable<I
void schemaChange(String statement, int instance);
+ void schemaChangeIgnoringStoppedInstances(String query);
+
int size();
Stream<I> stream();
@@ -55,6 +57,15 @@ public interface ICluster<I extends IInstance> extends
AutoCloseable, Iterable<I
Stream<I> stream(String dcName, String rackName);
+ IInstanceConfig newInstanceConfig();
+
+ IInstanceConfig createInstanceConfig(int nodeNum);
+
+ /**
+ * @return the first instance with running state
+ */
+ I getFirstRunningInstance();
+
@Override
default Iterator<I> iterator()
{
@@ -136,4 +147,4 @@ public interface ICluster<I extends IInstance> extends
AutoCloseable, Iterable<I
throw new RuntimeException(e);
}
}
-}
\ No newline at end of file
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/AbstractBuilder.java
b/src/main/java/org/apache/cassandra/distributed/shared/AbstractBuilder.java
index c9712df..2f9a2b8 100644
--- a/src/main/java/org/apache/cassandra/distributed/shared/AbstractBuilder.java
+++ b/src/main/java/org/apache/cassandra/distributed/shared/AbstractBuilder.java
@@ -68,6 +68,7 @@ public abstract class AbstractBuilder<I extends IInstance, C
extends ICluster, B
private boolean finalised;
private int tokenCount = getDefaultTokenCount();
private VNodeState vnodeState = VNodeState.SUPPORT_ALL;
+ private boolean dynamicPortAllocation = false;
protected int getDefaultTokenCount() {
String key = "cassandra.dtest.num_tokens";
@@ -160,6 +161,20 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
return vnodeState != VNodeState.ONLY_VNODE;
}
+ /**
+ * @return {@code true} if dynamic port allocation for the storage, native
and JMX will be use, {@code false}
+ * otherwise
+ */
+ public boolean isDynamicPortAllocation() {
+ return dynamicPortAllocation;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected B self()
+ {
+ return (B) this;
+ }
+
public C start() throws IOException
{
C cluster = createWithoutStarting();
@@ -180,37 +195,37 @@ public abstract class AbstractBuilder<I extends
IInstance, C extends ICluster, B
if (tokenSupplier == null)
tokenSupplier = evenlyDistributedTokens(nodeCount, tokenCount);
- return factory.newCluster((B) this);
+ return factory.newCluster(self());
}
public B withSharedClassLoader(ClassLoader sharedClassLoader)
{
this.sharedClassLoader = Objects.requireNonNull(sharedClassLoader,
"sharedClassLoader");
- return (B) this;
+ return self();
}
public B withSharedClasses(Predicate<String> sharedClasses)
{
this.sharedClasses = Objects.requireNonNull(sharedClasses,
"sharedClasses");
- return (B) this;
+ return self();
}
public B withBroadcastPort(int broadcastPort) {
this.broadcastPort = broadcastPort;
- return (B) this;
+ return self();
}
public B withTokenSupplier(TokenSupplier tokenSupplier)
{
this.tokenSupplier = tokenSupplier;
- return (B) this;
+ return self();
}
@Deprecated
public B withTokenSupplier(SingleTokenSupplier tokenSupplier)
{
this.tokenSupplier = tokenSupplier;
- return (B) this;
+ return self();
}
/**
@@ -232,7 +247,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
public B withSubnet(int subnet)
{
this.subnet = subnet;
- return (B) this;
+ return self();
}
/**
@@ -246,7 +261,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
public B withNodes(int nodeCount)
{
this.nodeCount = nodeCount;
- return (B) this;
+ return self();
}
/**
@@ -270,7 +285,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
for (int dc = 1; dc <= dcCount; dc++)
for (int rack = 1; rack <= racksPerDC; rack++)
withRack(dcName(dc), rackName(rack), -1);
- return (B) this;
+ return self();
}
/**
@@ -284,7 +299,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
for (int dc = 1; dc <= dcCount; dc++)
for (int rack = 1; rack <= racksPerDC; rack++)
withRack(dcName(dc), rackName(rack), nodesPerRack);
- return (B) this;
+ return self();
}
/**
@@ -301,7 +316,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
public B withRack(String dcName, String rackName, int nodesInRack)
{
racks.add(new Rack(dcName, rackName, nodesInRack));
- return (B) this;
+ return self();
}
// Map of node ids to dc and rack - must be contiguous with an entry
nodeId 1 to nodeCount
@@ -317,31 +332,31 @@ public abstract class AbstractBuilder<I extends
IInstance, C extends ICluster, B
this.nodeIdTopology = new HashMap<>(nodeIdTopology);
- return (B) this;
+ return self();
}
public B withRoot(File root)
{
this.rootFile = root;
- return (B) this;
+ return self();
}
public B withRoot(Path root)
{
this.rootPath = root;
- return (B) this;
+ return self();
}
public B withVersion(Versions.Version version)
{
this.version = version;
- return (B) this;
+ return self();
}
public B withConfig(Consumer<IInstanceConfig> updater)
{
this.configUpdater = updater;
- return (B) this;
+ return self();
}
public B appendConfig(Consumer<IInstanceConfig> updater)
@@ -349,7 +364,7 @@ public abstract class AbstractBuilder<I extends IInstance,
C extends ICluster, B
Consumer<IInstanceConfig> prev = configUpdater;
Consumer<IInstanceConfig> next = prev == null ? updater : config -> {
prev.accept(config); updater.accept(config); };
this.configUpdater = next;
- return (B) this;
+ return self();
}
public B withInstanceInitializer(BiConsumer<ClassLoader, Integer>
instanceInitializer)
@@ -365,45 +380,61 @@ public abstract class AbstractBuilder<I extends
IInstance, C extends ICluster, B
instanceInitializer.accept(classLoader, num);
}
};
- return (B) this;
+ return self();
}
public B withInstanceInitializer(IInstanceInitializer instanceInitializer)
{
this.instanceInitializer = instanceInitializer;
- return (B) this;
+ return self();
}
public B withClassTransformer(IClassTransformer classTransformer)
{
this.classTransformer = classTransformer;
- return (B) this;
+ return self();
}
public B withDataDirCount(int datadirCount)
{
assert datadirCount > 0 : "data dir count requires a positive number
but given " + datadirCount;
this.datadirCount = datadirCount;
- return (B) this;
+ return self();
}
public B withTokenCount(int tokenCount)
{
assert tokenCount > 0 : "Token count must be positive; given " +
tokenCount;
this.tokenCount = tokenCount;
- return (B) this;
+ return self();
}
public B withVNodes()
{
vnodeState = VNodeState.ONLY_VNODE;
- return (B) this;
+ return self();
}
public B withoutVNodes()
{
vnodeState = VNodeState.ONLY_SINGLE_TOKEN;
- return (B) this;
+ return self();
+ }
+
+ /**
+ * When {@code dynamicPortAllocation} is {@code true}, it will request to
dynamically provision
+ * available storage, native and JMX ports in the given interface. When
{@code dynamicPortAllocation} is
+ * {@code false} (the default behavior), it will use statically allocated
ports based on the number of
+ * interfaces available and the node number.
+ *
+ * @param dynamicPortAllocation {@code true} for dynamic port allocation,
{@code false} for static port
+ * allocation
+ * @return a reference to this Builder
+ */
+ public B withDynamicPortAllocation(boolean dynamicPortAllocation)
+ {
+ this.dynamicPortAllocation = dynamicPortAllocation;
+ return self();
}
private void finaliseBuilder()
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/Versions.java
b/src/main/java/org/apache/cassandra/distributed/shared/Versions.java
index f12a7b4..fca8fb6 100644
--- a/src/main/java/org/apache/cassandra/distributed/shared/Versions.java
+++ b/src/main/java/org/apache/cassandra/distributed/shared/Versions.java
@@ -123,17 +123,27 @@ public final class Versions
public Version getLatest(Semver version)
{
- return versions.get(first(version))
-
.stream()
-
.findFirst()
-
.orElseThrow(() -> new RuntimeException("No " + version + " versions
found"));
+ return versions.getOrDefault(first(version), Collections.emptyList())
+ .stream()
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No " + version
+ " versions found"));
}
public static Versions find()
{
final String dtestJarDirectory = System.getProperty(PROPERTY_PREFIX +
"test.dtest_jar_path", "build");
+ return find(dtestJarDirectory);
+ }
+
+ public static Versions find(String dtestJarDirectory)
+ {
final File sourceDirectory = new File(dtestJarDirectory);
- logger.info("Looking for dtest jars in " +
sourceDirectory.getAbsolutePath());
+ return find(sourceDirectory);
+ }
+
+ public static Versions find(File sourceDirectory)
+ {
+ logger.info("Looking for dtest jars in {}",
sourceDirectory.getAbsolutePath());
final Pattern pattern =
Pattern.compile("dtest-(?<fullversion>(\\d+)\\.(\\d+)((\\.|-alpha|-beta|-rc)([0-9]+))?(\\.\\d+)?)([~\\-]\\w[.\\w]*(?:\\-\\w[.\\w]*)*)?(\\+[.\\w]+)?\\.jar");
final Map<Semver, List<Version>> versions = new HashMap<>();
@@ -146,8 +156,8 @@ public final class Versions
continue;
Semver version = new Semver(m.group(1), SemverType.LOOSE);
Semver series = first(version);
- versions.putIfAbsent(series, new ArrayList<>());
- versions.get(series).add(new Version(version, new URL[]{
toURL(file) }));
+ versions.computeIfAbsent(series, k -> new ArrayList<>())
+ .add(new Version(version, new URL[]{ toURL(file) }));
}
}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingRMIServerSocketFactoryImpl.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingRMIServerSocketFactoryImpl.java
new file mode 100644
index 0000000..5577ab5
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingRMIServerSocketFactoryImpl.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.apache.cassandra.distributed.shared.jmx;
+
+import javax.net.ServerSocketFactory;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * This class is used to keep track of RMI servers created during a cluster
creation so we can
+ * later close the sockets, which would otherwise be left with a thread
running waiting for
+ * connections that would never show up as the server was otherwise closed.
+ */
+public class CollectingRMIServerSocketFactoryImpl implements
RMICloseableServerSocketFactory
+{
+ private final InetAddress bindAddress;
+ List<ServerSocket> sockets = new ArrayList<>();
+
+ public CollectingRMIServerSocketFactoryImpl(InetAddress bindAddress)
+ {
+ this.bindAddress = bindAddress;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int pPort) throws IOException
+ {
+ ServerSocket result =
ServerSocketFactory.getDefault().createServerSocket(pPort, 0, bindAddress);
+ try
+ {
+ result.setReuseAddress(true);
+ }
+ catch (SocketException e)
+ {
+ result.close();
+ throw e;
+ }
+ sockets.add(result);
+ return result;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ for (ServerSocket socket : sockets)
+ {
+ socket.close();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CollectingRMIServerSocketFactoryImpl that =
(CollectingRMIServerSocketFactoryImpl) o;
+ return Objects.equals(bindAddress, that.bindAddress);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(bindAddress);
+ }
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingSslRMIServerSocketFactoryImpl.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingSslRMIServerSocketFactoryImpl.java
new file mode 100644
index 0000000..2395d3a
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/CollectingSslRMIServerSocketFactoryImpl.java
@@ -0,0 +1,149 @@
+/*
+ * 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 org.apache.cassandra.distributed.shared.jmx;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * This class is used to keep track of SSL based RMI servers created during a
cluster creation to
+ * later close the sockets, which would otherwise be left with a thread
running waiting for
+ * connections that would never show up as the server was otherwise closed.
+ */
+public class CollectingSslRMIServerSocketFactoryImpl implements
RMICloseableServerSocketFactory
+{
+ private final InetAddress bindAddress;
+ private final String[] enabledCipherSuites;
+ private final String[] enabledProtocols;
+ private final boolean needClientAuth;
+ private final SSLSocketFactory sslSocketFactory;
+ List<ServerSocket> sockets = new ArrayList<>();
+
+ public CollectingSslRMIServerSocketFactoryImpl(InetAddress bindAddress,
String[] enabledCipherSuites,
+ String[] enabledProtocols,
boolean needClientAuth, SSLContext sslContext)
+ {
+ this.bindAddress = bindAddress;
+ this.enabledCipherSuites = enabledCipherSuites;
+ this.enabledProtocols = enabledProtocols;
+ this.needClientAuth = needClientAuth;
+ this.sslSocketFactory = sslContext.getSocketFactory();
+ }
+
+ public String[] getEnabledCipherSuites()
+ {
+ return enabledCipherSuites;
+ }
+
+ public String[] getEnabledProtocols()
+ {
+ return enabledProtocols;
+ }
+
+ public boolean isNeedClientAuth()
+ {
+ return needClientAuth;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int pPort) throws IOException
+ {
+ ServerSocket result = createSslServerSocket(pPort);
+ try
+ {
+ result.setReuseAddress(true);
+ }
+ catch (SocketException e)
+ {
+ result.close();
+ throw e;
+ }
+ sockets.add(result);
+ return result;
+ }
+
+ private ServerSocket createSslServerSocket(int pPort) throws IOException
+ {
+ return new ServerSocket(pPort, 0, bindAddress)
+ {
+ public Socket accept() throws IOException
+ {
+ Socket socket = super.accept();
+ SSLSocket sslSocket = (SSLSocket)
sslSocketFactory.createSocket(
+ socket, socket.getInetAddress().getHostName(),
+ socket.getPort(), true);
+ sslSocket.setUseClientMode(false);
+ if (enabledCipherSuites != null)
+ {
+ sslSocket.setEnabledCipherSuites(enabledCipherSuites);
+ }
+ if (enabledProtocols != null)
+ {
+ sslSocket.setEnabledProtocols(enabledProtocols);
+ }
+ sslSocket.setNeedClientAuth(needClientAuth);
+ return sslSocket;
+ }
+ };
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ for (ServerSocket socket : sockets)
+ {
+ socket.close();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CollectingSslRMIServerSocketFactoryImpl that =
(CollectingSslRMIServerSocketFactoryImpl) o;
+ return Objects.equals(bindAddress, that.bindAddress);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(bindAddress);
+ }
+
+ private static SSLSocketFactory defaultSSLSocketFactory = null;
+
+ private static synchronized SSLSocketFactory getDefaultSSLSocketFactory()
+ {
+ if (defaultSSLSocketFactory == null)
+ defaultSSLSocketFactory =
+ (SSLSocketFactory) SSLSocketFactory.getDefault();
+ return defaultSSLSocketFactory;
+ }
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMIClientSocketFactoryImpl.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMIClientSocketFactoryImpl.java
new file mode 100644
index 0000000..2f7bc8d
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMIClientSocketFactoryImpl.java
@@ -0,0 +1,82 @@
+package org.apache.cassandra.distributed.shared.jmx;
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class is used to override the local address the JMX client calculates
when trying to connect,
+ * which can otherwise be influenced by the system property
"java.rmi.server.hostname" in strange and
+ * unpredictable ways.
+ */
+public class RMIClientSocketFactoryImpl implements Serializable,
RMICloseableClientSocketFactory
+{
+ private static final long serialVersionUID = 955153017775496366L;
+ List<Socket> sockets = new ArrayList<>();
+ private final InetAddress localAddress;
+
+ public RMIClientSocketFactoryImpl(InetAddress localAddress)
+ {
+ this.localAddress = localAddress;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException
+ {
+ Socket socket = new Socket(localAddress, port);
+ sockets.add(socket);
+ return socket;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ for (Socket socket: sockets)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch (IOException ignored)
+ {
+ // intentionally ignored
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RMIClientSocketFactoryImpl that = (RMIClientSocketFactoryImpl) o;
+ return Objects.equals(localAddress, that.localAddress);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(localAddress);
+ }
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableClientSocketFactory.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableClientSocketFactory.java
new file mode 100644
index 0000000..eb0becd
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableClientSocketFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.apache.cassandra.distributed.shared.jmx;
+
+import java.rmi.server.RMIClientSocketFactory;
+
+/**
+ * This represents closeable RMI Client Socket factory. It extends {@link
AutoCloseable} and can be used with
+ * {@code try-with-resources}.
+ */
+public interface RMICloseableClientSocketFactory extends
RMIClientSocketFactory, AutoCloseable
+{
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableServerSocketFactory.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableServerSocketFactory.java
new file mode 100644
index 0000000..9c4b002
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMICloseableServerSocketFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.apache.cassandra.distributed.shared.jmx;
+
+import java.rmi.server.RMIServerSocketFactory;
+
+/**
+ * This represents closeable RMI Server Socket factory. It extends {@link
AutoCloseable} and can be used with
+ * {@code try-with-resources}.
+ */
+public interface RMICloseableServerSocketFactory extends
RMIServerSocketFactory, AutoCloseable
+{
+}
diff --git
a/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMISslClientSocketFactoryImpl.java
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMISslClientSocketFactoryImpl.java
new file mode 100644
index 0000000..7ceddb6
--- /dev/null
+++
b/src/main/java/org/apache/cassandra/distributed/shared/jmx/RMISslClientSocketFactoryImpl.java
@@ -0,0 +1,131 @@
+/*
+ * 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 org.apache.cassandra.distributed.shared.jmx;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+
+/**
+ * {@code RMIClientSocketFactory} for testing SSL based JMX clients.
+ * This class is used to override the local address the JMX client calculates
when trying to connect,
+ * which can otherwise be influenced by the system property
"java.rmi.server.hostname" in strange and
+ * unpredictable ways.
+ */
+public class RMISslClientSocketFactoryImpl implements Serializable,
RMICloseableClientSocketFactory
+{
+ private static final long serialVersionUID = 9054380061905145241L;
+ private static final Pattern COMMA_SPLITTER = Pattern.compile(",");
+ private static final List<Socket> sockets = new ArrayList<>();
+ private final InetAddress localAddress;
+ private final String[] enabledCipherSuites;
+ private final String[] enabledProtocols;
+
+ public RMISslClientSocketFactoryImpl(InetAddress localAddress, String
enabledCipherSuites, String enabledProtocls)
+ {
+ this.localAddress = localAddress;
+ this.enabledCipherSuites =
splitCommaSeparatedString(enabledCipherSuites);
+ this.enabledProtocols = splitCommaSeparatedString(enabledProtocls);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException
+ {
+ Socket socket = createSslSocket(port);
+ sockets.add(socket);
+ return socket;
+ }
+
+ private Socket createSslSocket(int port) throws IOException
+ {
+ final SocketFactory sslSocketFactory = SSLSocketFactory.getDefault();
+ final SSLSocket sslSocket = (SSLSocket)
sslSocketFactory.createSocket(localAddress, port);
+ if (enabledCipherSuites != null)
+ {
+ try
+ {
+ sslSocket.setEnabledCipherSuites(enabledCipherSuites);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+ if (enabledProtocols != null)
+ {
+ try
+ {
+ sslSocket.setEnabledProtocols(enabledProtocols);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+ return sslSocket;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ for (Socket socket : sockets)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch (IOException ignored)
+ {
+ // intentionally ignored
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RMISslClientSocketFactoryImpl that = (RMISslClientSocketFactoryImpl) o;
+ return Objects.equals(localAddress, that.localAddress);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(localAddress);
+ }
+
+ private String[] splitCommaSeparatedString(String stringToSplit)
+ {
+ if (stringToSplit == null)
+ return null;
+ return COMMA_SPLITTER.split(stringToSplit);
+ }
+}
diff --git
a/src/test/java/org/apache/cassandra/distributed/shared/VersionsTest.java
b/src/test/java/org/apache/cassandra/distributed/shared/VersionsTest.java
index 0bdb57e..65d30c1 100644
--- a/src/test/java/org/apache/cassandra/distributed/shared/VersionsTest.java
+++ b/src/test/java/org/apache/cassandra/distributed/shared/VersionsTest.java
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class VersionsTest
{
@@ -42,15 +43,17 @@ public class VersionsTest
"4.0.0",
"4.1.0"
};
+ private static String dtestJarPath;
@BeforeAll
public static void beforeAll() throws IOException
{
Path root = Files.createTempDirectory("versions");
- System.setProperty(Versions.PROPERTY_PREFIX + "test.dtest_jar_path",
root.toAbsolutePath().toString());
+ dtestJarPath = root.toAbsolutePath().toString();
+ System.setProperty(Versions.PROPERTY_PREFIX + "test.dtest_jar_path",
dtestJarPath);
for (String version : VERSIONS)
- Files.createFile(Paths.get(root.toAbsolutePath().toString(),
"dtest-" + version + ".jar"));
+ Files.createFile(Paths.get(dtestJarPath, "dtest-" + version +
".jar"));
}
@AfterAll
@@ -80,6 +83,16 @@ public class VersionsTest
Versions.find().getLatest(new Semver("2.2", Semver.SemverType.LOOSE));
}
+ @Test
+ public void testGetLatestShouldNotThrowNPEWhenNoJarsAreFound()
+ {
+ System.setProperty(Versions.PROPERTY_PREFIX + "test.dtest_jar_path",
"non-existent-path");
+ assertThatExceptionOfType(RuntimeException.class).isThrownBy(() ->
Versions.find()
+
.getLatest(new Semver("4.0.0")))
+ .withMessage("No
4.0.0 versions found");
+ System.setProperty(Versions.PROPERTY_PREFIX + "test.dtest_jar_path",
dtestJarPath);
+ }
+
@Test
public void testFind()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]