blob: 9a88c5b76dc57f80deadcf892abf24bc3a1843df [file] [log] [blame]
/*
* 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.sidecar.common;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import com.google.common.collect.Sets;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.Preconditions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/***
* In order to support multiple versions of Cassandra in the Sidecar, we would like to avoid depending directly on
* any Cassandra code.
* Additionally, whe would like to avoid copy/pasting the entire MBean interface classes into the Sidecar.
* This test exists to prove out some assumptions about using matching sub-interfaces (or even functional interfaces)
* to make JMX calls. This particular call happens to match the signature of the `importNewSSTables` method on
* StorageServiceProxy in C* 4.0.
*/
public class JmxClientTest
{
private static final JMXServiceURL serviceURL;
private static final String objectName = "org.apache.cassandra.jmx:type=ExtendedImport";
public static final int PROXIES_TO_TEST = 10_000;
private static StorageService importMBean;
private static JMXConnectorServer jmxServer;
private static MBeanServer mbs;
private static Registry registry;
@BeforeAll
public static void setUp() throws Exception
{
System.setProperty("java.rmi.server.randomIds", "true");
String passwordFile = Objects.requireNonNull(JmxClientTest.class
.getClassLoader()
.getResource("testJmxPassword.properties")).getPath();
Map<String, String> env = new HashMap<>();
env.put("jmx.remote.x.password.file", passwordFile);
registry = LocateRegistry.createRegistry(9999);
mbs = ManagementFactory.getPlatformMBeanServer();
jmxServer = JMXConnectorServerFactory.newJMXConnectorServer(serviceURL, env, mbs);
jmxServer.start();
importMBean = new StorageService();
mbs.registerMBean(importMBean, new ObjectName(objectName));
}
@AfterAll
public static void tearDown() throws Exception
{
jmxServer.stop();
final ObjectName name = new ObjectName(objectName);
if (mbs.isRegistered(name))
{
mbs.unregisterMBean(name);
}
UnicastRemoteObject.unexportObject(registry, true);
registry = null;
}
@BeforeEach
public void setup()
{
importMBean.shouldSucceed = true;
}
@Test
public void testCanCallMethodWithoutEntireInterface()
{
JmxClient client = new JmxClient(serviceURL, "controlRole", "password");
List<String> result = client.proxy(Import.class, objectName)
.importNewSSTables(Sets.newHashSet("foo", "bar"), true,
true, true, true, true,
true);
assertThat(result.size()).isEqualTo(0);
}
@Test
public void testCanCallMethodWithoutEntireInterfaceGetResults()
{
importMBean.shouldSucceed = false;
JmxClient client = new JmxClient(serviceURL, "controlRole", "password");
final HashSet<String> srcPaths = Sets.newHashSet("foo", "bar");
final List<String> failedDirs = client.proxy(Import.class, objectName)
.importNewSSTables(srcPaths, true,
true, true, true, true,
true);
assertThat(failedDirs.size()).isEqualTo(2);
assertThat(failedDirs.toArray()).isEqualTo(srcPaths.toArray());
}
@Test
public void testCallWithoutCredentialsFails()
{
assertThatExceptionOfType(SecurityException.class)
.isThrownBy(() ->
{
JmxClient client = new JmxClient(serviceURL);
client.proxy(Import.class, objectName)
.importNewSSTables(Sets.newHashSet("foo", "bar"),
true,
true,
true,
true,
true,
true);
});
}
@Test
public void testDisconnectReconnect() throws Exception
{
JmxClient client = new JmxClient(serviceURL, "controlRole", "password");
assertThat(client.isConnected()).isFalse();
List<String> result = client.proxy(Import.class, objectName)
.importNewSSTables(
Sets.newHashSet("foo", "bar"), true, true, true,
true, true,
true);
assertThat(client.isConnected()).isTrue();
assertThat(result.size()).isEqualTo(0);
tearDown();
setUp();
result = client.proxy(Import.class, objectName)
.importNewSSTables(
Sets.newHashSet("foo", "bar"), true, true, true,
true, true,
true);
assertThat(result.size()).isEqualTo(0);
}
@Test
public void testLotsOfProxies()
{
JmxClient client = new JmxClient(serviceURL, "controlRole", "password");
for (int i = 0; i < PROXIES_TO_TEST; i++)
{
List<String> result = client.proxy(Import.class, objectName)
.importNewSSTables(
Sets.newHashSet("foo", "bar"), true, true, true,
true, true,
true);
assertThat(result).isNotNull();
}
}
/**
* Simulates to C*'s `nodetool import` call
*/
public interface Import
{
List<String> importNewSSTables(Set<String> srcPaths, boolean resetLevel, boolean clearRepaired,
boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches,
boolean extendedVerify);
}
/**
* Simulates the larger Storage Service MBean interface
*/
public interface StorageServiceMBean
{
List<String> importNewSSTables(Set<String> srcPaths, boolean resetLevel, boolean clearRepaired,
boolean verifySSTables, boolean verifyTokens, boolean invalidateCaches,
boolean extendedVerify);
void someOtherMethod(String helloString);
}
/**
* An implementation of our mock StorageServiceMBean
*/
public static class StorageService implements StorageServiceMBean
{
private static final Logger logger = Logger.getLogger(StorageService.class.getSimpleName());
public boolean shouldSucceed = true;
@Override
public List<String> importNewSSTables(Set<String> srcPaths, boolean resetLevel, boolean clearRepaired,
boolean verifySSTables, boolean verifyTokens,
boolean invalidateCaches, boolean extendedVerify)
{
Preconditions.notNull(srcPaths, "Source Paths missing");
if (shouldSucceed)
{
return Collections.emptyList();
}
return Arrays.asList(srcPaths.toArray(new String[0]));
}
@Override
public void someOtherMethod(String helloString)
{
logger.info(helloString);
}
}
static
{
try
{
serviceURL = new JMXServiceURL("service:jmx:rmi://localhost/jndi/rmi://localhost:9999/jmxrmi");
}
catch (MalformedURLException e)
{
throw new RuntimeException(e);
}
}
}