Add integration tests task
Patch by Chris Lohfink, reviewed by Dinesh Joshi and Vinay Chella for CASSANDRA-15031
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..8ab909d
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,43 @@
+# Java Gradle CircleCI 2.0 configuration file
+#
+# Check https://circleci.com/docs/2.0/language-java/ for more details
+#
+version: 2
+jobs:
+ build:
+ docker:
+ - image: circleci/openjdk:8-jdk
+
+ working_directory: ~/repo
+
+ environment:
+ TERM: dumb
+
+ steps:
+ - checkout
+
+ # Download and cache dependencies
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "build.gradle" }}
+ # fallback to using the latest cache if no exact match is found
+ - v1-dependencies-
+
+ - run: ./gradlew dependencies
+
+ - save_cache:
+ paths:
+ - ~/.gradle
+ key: v1-dependencies-{{ checksum "build.gradle" }}
+
+ # make sure it builds with build steps like swagger docs and dist
+ - run: ./gradlew build
+
+ # run tests!
+ - run: ./gradlew check
+
+ - store_artifacts:
+ path: build/reports
+ destination: test-reports
+ - store_test_results:
+ path: build/reports
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d08471c..f5394e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@
/.ant-targets-build.xml
# Generated files from the documentation
-doc/source/configuration/cassandra_config_file.rst
-doc/source/tools/nodetool
+src/main/resources/docs/*
+src/dist/*
+*.logdir_IS_UNDEFINED
diff --git a/build.gradle b/build.gradle
index a479649..093cca5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,9 @@
id 'java'
id 'application'
id 'idea'
+ id 'checkstyle'
+ id 'jacoco'
+ id 'findbugs'
id 'org.hidetake.swagger.generator' version '2.16.0'
}
@@ -52,10 +55,23 @@
srcDirs = [main.resources, "src/test/resources"]
}
}
+ integrationTest {
+ java {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ srcDir file('src/integration/java')
+ }
+ resources {
+ srcDirs = [main.resources, "src/integration/resources"]
+ }
+ }
}
configurations {
jolokia
+
+ integrationTestCompile.extendsFrom testCompile
+ integrationTestRuntime.extendsFrom testRuntime
}
dependencies {
@@ -82,6 +98,8 @@
testCompile 'org.apache.commons:commons-exec:1.3+'
testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
testCompile group: 'io.vertx', name: 'vertx-junit5', version: '3.6.3'
+
+ integrationTestCompile group: 'com.datastax.oss.simulacron', name: 'simulacron-driver-3x', version: '0.8.7'
}
swaggerSources {
@@ -124,7 +142,6 @@
delete "$projectDir/src/dist/agents"
println "Deleting generated docs $projectDir/src/main/resources/docs"
delete "$projectDir/src/main/resources/docs"
-
}
test {
@@ -133,6 +150,29 @@
systemProperty "javax.net.ssl.trustStorePassword", "password"
}
+task integrationTest(type: Test) {
+ jacoco {
+ enabled = false
+ }
+ useJUnitPlatform()
+ testClassesDirs = sourceSets.integrationTest.output.classesDirs
+ classpath = sourceSets.integrationTest.runtimeClasspath
+ shouldRunAfter test
+}
+
+checkstyle {
+ toolVersion '7.8.1'
+ configFile file("checkstyle.xml")
+}
+
+tasks.withType(FindBugs) {
+ reports {
+ xml.enabled false
+ html.enabled true
+ }
+}
+
// copyDist gets called on every build
copyDist.dependsOn installDist
+check.dependsOn checkstyleMain, checkstyleTest, integrationTest, jacocoTestReport
build.dependsOn copyDist, generateReDoc, generateSwaggerUI, copyJolokia
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..6206060
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,393 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ # Copyright 2015 WSO2 Inc. (http://wso2.org)
+ #
+ # Licensed under the Apache License, Version 2.0 (the "License");
+ # you may not use this file except in compliance with the License.
+ # You may obtain a copy of the License at
+ #
+ # http://www.apache.org/licenses/LICENSE-2.0
+ #
+ # Unless required by applicable law or agreed to in writing, software
+ # distributed under the License is distributed on an "AS IS" BASIS,
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ # See the License for the specific language governing permissions and
+ # limitations under the License.
+-->
+
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!-- This is a checkstyle configuration file. For descriptions of
+what the following rules do, please see the checkstyle configuration
+page at http://checkstyle.sourceforge.net/config.html -->
+
+<module name="Checker">
+
+ <module name="FileTabCharacter">
+ <property name="severity" value="error" />
+ <!-- Checks that there are no tab characters in the file.
+ -->
+ </module>
+
+ <!--
+
+ LENGTH CHECKS FOR FILES
+
+ -->
+
+ <module name="FileLength">
+ <property name="max" value="3000" />
+ <property name="severity" value="warning" />
+ </module>
+
+
+ <module name="NewlineAtEndOfFile">
+ <property name="lineSeparator" value="lf" />
+ </module>
+
+ <module name="RegexpSingleline">
+ <!-- Checks that FIXME is not used in comments. TODO is preferred.
+ -->
+ <property name="format" value="((//.*)|(\*.*))FIXME" />
+ <property name="message" value='TODO is preferred to FIXME. e.g. "TODO: (ENG-123) - Refactor when v2 is released."' />
+ </module>
+
+ <module name="RegexpSingleline">
+ <!-- Checks that TODOs are named with some basic formatting. Checks for the following pattern TODO: (
+ -->
+ <property name="format" value="((//.*)|(\*.*))TODO[^: (]" />
+ <property name="message" value='All TODOs should be named. e.g. "TODO: (ENG-123) - Refactor when v2 is released."' />
+ </module>
+
+ <!--<module name="JavadocPackage">-->
+ <!--<!– Checks that each Java package has a Javadoc file used for commenting.-->
+ <!--Only allows a package-info.java, not package.html. –>-->
+ <!--<property name="severity" value="warning"/>-->
+ <!--</module>-->
+
+ <!-- All Java AST specific tests live under TreeWalker module. -->
+ <module name="TreeWalker">
+
+ <!-- required for SupressionCommentFilter and SuppressWithNearbyCommentFilter -->
+ <module name="FileContentsHolder" />
+
+ <!--
+
+ IMPORT CHECKS
+
+ -->
+
+ <module name="AvoidStarImport">
+ <property name="allowClassImports" value="false" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="RedundantImport">
+ <!-- Checks for redundant import statements. -->
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="ImportOrder">
+ <!-- Checks for out of order import statements. -->
+ <property name="severity" value="error" />
+ <property name="ordered" value="true" />
+ <property name="groups" value="/^java\./,javax,com.google.common,org.apache.commons,org.junit,org.slf4j" />
+ <!-- This ensures that static imports go to the end. -->
+ <property name="option" value="bottom" />
+ <property name="tokens" value="STATIC_IMPORT, IMPORT" />
+ </module>
+
+ <module name="IllegalImport">
+ <property name="illegalPkgs" value="junit.framework" />
+ </module>
+
+ <module name="UnusedImports" />
+
+ <!--
+
+ METHOD LENGTH CHECKS
+
+ -->
+
+ <module name="MethodLength">
+ <property name="tokens" value="METHOD_DEF" />
+ <property name="max" value="300" />
+ <property name="countEmpty" value="false" />
+ <property name="severity" value="warning" />
+ </module>
+
+ <!--
+
+ JAVADOC CHECKS
+
+ -->
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <module name="JavadocMethod">
+ <property name="scope" value="protected" />
+ <property name="severity" value="error" />
+ <property name="allowMissingJavadoc" value="true" />
+ <property name="allowMissingParamTags" value="true" />
+ <property name="allowMissingReturnTag" value="true" />
+ <property name="allowMissingThrowsTags" value="true" />
+ <property name="allowThrowsTagsForSubclasses" value="true" />
+ <property name="allowUndeclaredRTE" value="true" />
+ </module>
+
+ <module name="JavadocType">
+ <property name="scope" value="protected" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="JavadocStyle">
+ <property name="checkFirstSentence" value="false" />
+ <property name="severity" value="error" />
+ </module>
+
+ <!--
+
+ NAMING CHECKS
+
+ -->
+
+ <!-- Item 38 - Adhere to generally accepted naming conventions -->
+
+ <module name="PackageName">
+ <!-- Validates identifiers for package names against the
+ supplied expression. -->
+ <!-- Here the default checkstyle rule restricts package name parts to
+ seven characters, this is not in line with common practice at Google.
+ -->
+ <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="TypeNameCheck">
+ <!-- Validates static, final fields against the
+ expression "^[A-Z][a-zA-Z0-9]*$". -->
+ <metadata name="altname" value="TypeName" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="ConstantNameCheck">
+ <!-- Validates non-private, static, final fields against the supplied
+ public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
+ <metadata name="altname" value="ConstantName" />
+ <property name="applyToPublic" value="true" />
+ <property name="applyToProtected" value="true" />
+ <property name="applyToPackage" value="true" />
+ <property name="applyToPrivate" value="false" />
+ <property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$" />
+ <message key="name.invalidPattern"
+ value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)." />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="StaticVariableNameCheck">
+ <!-- Validates static, non-final fields against the supplied
+ expression "^[a-z][a-zA-Z0-9]*_?$". -->
+ <metadata name="altname" value="StaticVariableName" />
+ <property name="applyToPublic" value="true" />
+ <property name="applyToProtected" value="true" />
+ <property name="applyToPackage" value="true" />
+ <property name="applyToPrivate" value="true" />
+ <property name="format" value="^[a-z][a-zA-Z0-9]*_?$" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="MemberNameCheck">
+ <!-- Validates non-static members against the supplied expression. -->
+ <metadata name="altname" value="MemberName" />
+ <property name="applyToPublic" value="true" />
+ <property name="applyToProtected" value="true" />
+ <property name="applyToPackage" value="true" />
+ <property name="applyToPrivate" value="true" />
+ <property name="format" value="^[a-z][a-zA-Z0-9]*$" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="MethodNameCheck">
+ <!-- Validates identifiers for method names. -->
+ <metadata name="altname" value="MethodName" />
+ <property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="ParameterName">
+ <!-- Validates identifiers for method parameters against the
+ expression "^[a-z][a-zA-Z0-9]*$". -->
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="LocalFinalVariableName">
+ <!-- Validates identifiers for local final variables against the
+ expression "^[a-z][a-zA-Z0-9]*$". -->
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="LocalVariableName">
+ <!-- Validates identifiers for local variables against the
+ expression "^[a-z][a-zA-Z0-9]*$". -->
+ <property name="severity" value="error" />
+ </module>
+
+
+ <!--
+
+ LENGTH and CODING CHECKS
+
+ -->
+
+ <module name="LineLength">
+ <!-- Checks if a line is too long. -->
+ <property name="max" value="120" default="120" />
+ <property name="severity" value="error" />
+
+ <!--
+ The default ignore pattern exempts the following elements:
+ - import statements
+ - long URLs inside comments
+ -->
+
+ <property name="ignorePattern" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
+ default="^(package .*;\s*)|(import .*;\s*)|( *\* *https?://.*)$" />
+ </module>
+
+ <module name="LeftCurly">
+ <!-- Checks for placement of the left curly brace ('{'). -->
+ <property name="option" value="nl"/>
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="RightCurly">
+ <property name="option" value="alone"/>
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="UpperEll">
+ <!-- Checks that long constants are defined with an upper ell.-->
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="FallThrough">
+ <!-- Warn about falling through to the next case statement. Similar to
+ javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
+ on the last non-blank line preceding the fallen-into case contains 'fall through' (or
+ some other variants which we don't publicized to promote consistency).
+ -->
+ <property name="reliefPattern"
+ value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on" />
+ <property name="severity" value="error" />
+ </module>
+
+
+ <!--
+
+ MODIFIERS CHECKS
+
+ -->
+
+ <module name="ModifierOrder">
+ <!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
+ 8.4.3. The prescribed order is:
+ public, protected, private, abstract, static, final, transient, volatile,
+ synchronized, native, strictfp
+ -->
+ </module>
+
+
+ <!--
+
+ WHITESPACE CHECKS
+
+ -->
+ <module name="GenericWhitespace" />
+
+ <module name="WhitespaceAround">
+ <!-- Checks that various tokens are surrounded by whitespace.
+ This includes most binary operators and keywords followed
+ by regular or curly braces.
+ -->
+ <property name="tokens"
+ value="ASSIGN, BAND, BAND_ASSIGN, BOR,
+ BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
+ EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
+ LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
+ LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
+ MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
+ SL, SLIST, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN" />
+ <property name="allowEmptyConstructors" value="true" />
+ <property name="allowEmptyMethods" value="true" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="WhitespaceAfter">
+ <!-- Checks that commas, semicolons and typecasts are followed by
+ whitespace.
+ -->
+ <property name="tokens" value="COMMA, SEMI, TYPECAST" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="NoWhitespaceAfter">
+ <!-- Checks that there is no whitespace after various unary operators.
+ Linebreaks are allowed.
+ -->
+ <property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
+ UNARY_PLUS" />
+ <property name="allowLineBreaks" value="true" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="NoWhitespaceBefore">
+ <!-- Checks that there is no whitespace before various unary operators.
+ Linebreaks are allowed.
+ -->
+ <property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC" />
+ <property name="allowLineBreaks" value="true" />
+ <property name="severity" value="error" />
+ </module>
+
+ <module name="ParenPad">
+ <!-- Checks that there is no whitespace before close parens or after
+ open parens.
+ -->
+ <property name="severity" value="error" />
+ </module>
+
+ <!-- No System.out -->
+ <module name="Regexp">
+ <property name="format" value="System\.out\.println" />
+ <property name="illegalPattern" value="true" />
+ </module>
+
+ <!-- No System.err -->
+ <module name="Regexp">
+ <!-- . matches any character, so we need to escape it and use \. to match dots. -->
+ <property name="format" value="System\.err\.println" />
+ <property name="illegalPattern" value="true" />
+ </module>
+
+ <!-- No printStackTrace -->
+ <module name="Regexp">
+ <!-- . matches any character, so we need to escape it and use \. to match dots. -->
+ <property name="format" value="e\.printStackTrace\(\)" />
+ <property name="illegalPattern" value="true" />
+ </module>
+ </module>
+
+ <!--module name="SuppressionFilter">
+ <property name="file" value="suppressions.xml"/>
+ </module-->
+
+ <module name="SuppressionCommentFilter">
+ <property name="offCommentFormat" value="CHECKSTYLE OFF: (.+)" />
+ <property name="onCommentFormat" value="CHECKSTYLE ON" />
+ <property name="checkFormat" value="Javadoc.*" />
+ <property name="messageFormat" value="$1" />
+ </module>
+
+</module>
\ No newline at end of file
diff --git a/conf/logback.xml b/conf/logback.xml
index bd0e398..df0e2b1 100644
--- a/conf/logback.xml
+++ b/conf/logback.xml
@@ -72,5 +72,7 @@
<appender-ref ref="STDOUT" />
</root>
- <logger name="org.apache.cassandra" level="DEBUG"/>
+
+ <logger name="com.datastax.driver.core" level="ERROR" />
+ <logger name="com.datastax.driver.core.ControlConnection" level="OFF" />
</configuration>
diff --git a/src/integration/java/org/apache/cassandra/sidecar/HealthServiceIntegrationTest.java b/src/integration/java/org/apache/cassandra/sidecar/HealthServiceIntegrationTest.java
new file mode 100644
index 0000000..567f67e
--- /dev/null
+++ b/src/integration/java/org/apache/cassandra/sidecar/HealthServiceIntegrationTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.datastax.driver.core.NettyOptions;
+import com.datastax.oss.simulacron.common.cluster.ClusterSpec;
+import com.datastax.oss.simulacron.server.BoundCluster;
+import com.datastax.oss.simulacron.server.BoundNode;
+import com.datastax.oss.simulacron.server.NodePerPortResolver;
+import com.datastax.oss.simulacron.server.Server;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.util.HashedWheelTimer;
+import io.netty.util.Timer;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.codec.BodyCodec;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.routes.HealthCheck;
+import org.apache.cassandra.sidecar.routes.HealthService;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Longer run and more intensive tests for the HealthService and HealthCheck
+ */
+@DisplayName("Health Service Integration Tests")
+public class HealthServiceIntegrationTest
+{
+ private static final ThreadFactory threadFactory = new ThreadFactoryBuilder()
+ .setDaemon(true)
+ .setNameFormat("HealthServiceTest-%d")
+ .build();
+ private static final HashedWheelTimer sharedHWT = new HashedWheelTimer(threadFactory);
+ private static final EventLoopGroup sharedEventLoopGroup = new NioEventLoopGroup(0, threadFactory);
+ private static final NettyOptions shared = new NettyOptions()
+ {
+ public EventLoopGroup eventLoopGroup(ThreadFactory threadFactory)
+ {
+ return sharedEventLoopGroup;
+ }
+
+ public void onClusterClose(EventLoopGroup eventLoopGroup)
+ {
+ }
+
+ public Timer timer(ThreadFactory threadFactory)
+ {
+ return sharedHWT;
+ }
+
+ public void onClusterClose(Timer timer)
+ {
+ }
+ };
+
+ private Vertx vertx;
+ private Router router;
+ private HttpServer httpServer;
+ private int port;
+ private List<CQLSession> sessions = new LinkedList<>();
+
+ @BeforeEach
+ void setUp() throws IOException
+ {
+ vertx = Vertx.vertx();
+ router = Router.router(vertx);
+ ServerSocket socket = new ServerSocket(0);
+ port = socket.getLocalPort();
+ httpServer = vertx.createHttpServer(new HttpServerOptions()
+ .setPort(port)
+ .setLogActivity(true));
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ vertx.close();
+ }
+
+ @AfterEach
+ public void closeClusters()
+ {
+ for (CQLSession session : sessions)
+ session.close();
+ sessions.clear();
+ }
+
+ @DisplayName("100 node cluster stopping, then starting")
+ @Test
+ public void testDownHost() throws InterruptedException
+ {
+ int nodeCount = 100;
+ try (Server server = Server.builder()
+ .withMultipleNodesPerIp(true)
+ .withAddressResolver(new NodePerPortResolver(new byte[]{ 127, 0, 0, 1 }, 49152))
+ .build())
+ {
+ ClusterSpec cluster = ClusterSpec.builder()
+ .withNodes(nodeCount)
+ .build();
+ BoundCluster bCluster = server.register(cluster);
+
+ Set<BoundNode> downNodes = new HashSet<>();
+ Map<BoundNode, HealthCheck> checks = new HashMap<>();
+
+ // Create a HealthCheck per node
+ for (BoundNode node : bCluster.getNodes())
+ checks.put(node, healthCheckFor(node, shared));
+
+ // verify all nodes marked as up
+ for (BoundNode node : bCluster.getNodes())
+ assertTrue(checks.get(node).get());
+
+ // shut down nodes one at a time, and verify we get correct response on all HealthChecks every iteration
+ for (int i = 0; downNodes.size() < nodeCount; i++)
+ {
+ for (BoundNode node : bCluster.getNodes())
+ assertEquals(checks.get(node).get(), !downNodes.contains(node));
+ bCluster.node(i).stop();
+ downNodes.add(bCluster.node(i));
+ }
+
+ // all hosts should be down
+ for (BoundNode node : bCluster.getNodes())
+ assertFalse(checks.get(node).get());
+
+ for (int i = 0; downNodes.size() > 0; i++)
+ {
+ bCluster.node(i).start();
+ downNodes.remove(bCluster.node(i));
+ }
+
+ // verify all nodes marked as up
+ long start = System.currentTimeMillis();
+ for (BoundNode node : bCluster.getNodes())
+ {
+ while ((System.currentTimeMillis() - start) < 10000 && !checks.get(node).get())
+ Thread.sleep(100);
+ assertTrue(checks.get(node).get());
+ }
+ }
+ }
+
+
+ @DisplayName("Down on startup, then comes up")
+ @Test
+ public void testDownHostTurnsOn() throws Throwable
+ {
+ VertxTestContext testContext = new VertxTestContext();
+ try (Server server = Server.builder()
+ .withMultipleNodesPerIp(true)
+ .withAddressResolver(new NodePerPortResolver(new byte[]{ 127, 0, 0, 1 }, 49152))
+ .build())
+ {
+ ClusterSpec cluster = ClusterSpec.builder()
+ .withNodes(1)
+ .build();
+ BoundCluster bCluster = server.register(cluster);
+
+ BoundNode node = bCluster.node(0);
+ node.stop();
+ CQLSession session = new CQLSession(node.inetSocketAddress(), shared);
+ sessions.add(session);
+ HealthCheck check = new HealthCheck(session);
+ HealthService service = new HealthService(new Configuration.Builder()
+ .setHealthCheckFrequency(1000)
+ .build(),
+ check, session);
+ service.start();
+ try
+ {
+ router.route("/health").handler(service::handleHealth);
+ httpServer.requestHandler(router);
+ httpServer.listen();
+
+ WebClient client = WebClient.create(vertx);
+ long start = System.currentTimeMillis();
+ client.get(port, "localhost", "/health")
+ .as(BodyCodec.string())
+ .send(testContext.succeeding(response -> testContext.verify(() ->
+ {
+ assertEquals(503, response.statusCode());
+
+ node.start();
+ while ((System.currentTimeMillis() - start) < (1000 * 60 * 2) && !check.get())
+ Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
+ service.refreshNow();
+ client.get(port, "localhost", "/health")
+ .as(BodyCodec.string())
+ .send(testContext.succeeding(upResponse -> testContext.verify(() ->
+ {
+ assertEquals(200, upResponse.statusCode());
+ testContext.completeNow();
+ })));
+ })));
+ assertTrue(testContext.awaitCompletion(125, TimeUnit.SECONDS));
+ if (testContext.failed())
+ {
+ throw testContext.causeOfFailure();
+ }
+ }
+ finally
+ {
+ service.stop();
+ }
+ }
+ }
+
+ public HealthCheck healthCheckFor(BoundNode node, NettyOptions shared)
+ {
+ CQLSession session = new CQLSession(node.inetSocketAddress(), shared);
+ sessions.add(session);
+ return new HealthCheck(session);
+ }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/CQLSession.java b/src/main/java/org/apache/cassandra/sidecar/CQLSession.java
new file mode 100644
index 0000000..b156547
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/CQLSession.java
@@ -0,0 +1,115 @@
+package org.apache.cassandra.sidecar;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.NettyOptions;
+import com.datastax.driver.core.QueryOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.policies.ExponentialReconnectionPolicy;
+import com.datastax.driver.core.policies.ReconnectionPolicy;
+import com.datastax.driver.core.policies.RoundRobinPolicy;
+import com.datastax.driver.core.policies.WhiteListPolicy;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Represents a connection to Cassandra cluster. Currently supports returning the local connection only as
+ * defined in the Configuration.
+ */
+@Singleton
+public class CQLSession
+{
+ private static final Logger logger = LoggerFactory.getLogger(CQLSession.class);
+ @Nullable
+ private Session localSession;
+ private final InetSocketAddress inet;
+ private final WhiteListPolicy wlp;
+ private NettyOptions nettyOptions;
+ private QueryOptions queryOptions;
+ private ReconnectionPolicy reconnectionPolicy;
+
+ @Inject
+ public CQLSession(Configuration configuration)
+ {
+ inet = InetSocketAddress.createUnresolved(configuration.getCassandraHost(), configuration.getCassandraPort());
+ wlp = new WhiteListPolicy(new RoundRobinPolicy(), Collections.singletonList(inet));
+ this.nettyOptions = new NettyOptions();
+ this.queryOptions = new QueryOptions().setConsistencyLevel(ConsistencyLevel.ONE);
+ this.reconnectionPolicy = new ExponentialReconnectionPolicy(1000,
+ configuration.getHealthCheckFrequencyMillis());
+ }
+
+ @VisibleForTesting
+ CQLSession(InetSocketAddress target, NettyOptions options)
+ {
+ inet = target;
+ wlp = new WhiteListPolicy(new RoundRobinPolicy(), Collections.singletonList(inet));
+ this.nettyOptions = options;
+ this.queryOptions = new QueryOptions().setConsistencyLevel(ConsistencyLevel.ONE);
+ reconnectionPolicy = new ExponentialReconnectionPolicy(100, 1000);
+ }
+
+ /**
+ * Provides a Session connected only to the local node from configuration. If null it means the the connection was
+ * not able to be established. The session still might throw a NoHostAvailableException if the local host goes
+ * offline or otherwise unavailable.
+ *
+ * @return Session
+ */
+ @Nullable
+ public synchronized Session getLocalCql()
+ {
+ Cluster cluster = null;
+ try
+ {
+ if (localSession == null)
+ {
+ cluster = Cluster.builder()
+ .addContactPointsWithPorts(inet)
+ .withLoadBalancingPolicy(wlp)
+ .withQueryOptions(queryOptions)
+ .withReconnectionPolicy(reconnectionPolicy)
+ .withoutMetrics()
+ // tests can create a lot of these Cluster objects, to avoid creating HWTs and
+ // event thread pools for each we have the override
+ .withNettyOptions(nettyOptions)
+ .build();
+ localSession = cluster.connect();
+ }
+ }
+ catch (Exception e)
+ {
+ logger.debug("Failed to reach Cassandra", e);
+ if (cluster != null)
+ {
+ try
+ {
+ cluster.close();
+ }
+ catch (Exception ex)
+ {
+ logger.debug("Failed to close cluster in cleanup", ex);
+ }
+ }
+ }
+ return localSession;
+ }
+
+ public synchronized void close()
+ {
+ if (localSession != null)
+ {
+ localSession.getCluster().close();
+ localSession = null;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
index aaa39f3..a06e3f6 100644
--- a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
+++ b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
@@ -18,6 +18,11 @@
package org.apache.cassandra.sidecar;
+import java.io.PrintStream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -25,9 +30,9 @@
import org.apache.cassandra.sidecar.routes.HealthService;
import org.apache.cassandra.sidecar.utils.SslUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+/**
+ * Main class for initiating the Cassandra sidecar
+ */
@Singleton
public class CassandraSidecarDaemon
{
@@ -46,7 +51,7 @@
public void start()
{
- banner();
+ banner(System.out);
validate();
logger.info("Starting Cassandra Sidecar on port {}", config.getPort());
healthService.start();
@@ -60,16 +65,16 @@
server.close();
}
- private void banner()
+ private void banner(PrintStream out)
{
- System.out.println(" _____ _ _____ _ _ \n" +
- "/ __ \\ | | / ___(_) | | \n" +
- "| / \\/ __ _ ___ ___ __ _ _ __ __| |_ __ __ _ \\ `--. _ __| | ___ ___ __ _ _ __ \n" +
- "| | / _` / __/ __|/ _` | '_ \\ / _` | '__/ _` | `--. \\ |/ _` |/ _ \\/ __/ _` | '__|\n" +
- "| \\__/\\ (_| \\__ \\__ \\ (_| | | | | (_| | | | (_| | /\\__/ / | (_| | __/ (_| (_| | | \n" +
- " \\____/\\__,_|___/___/\\__,_|_| |_|\\__,_|_| \\__,_| \\____/|_|\\__,_|\\___|\\___\\__,_|_| \n" +
- " \n" +
- " ");
+ out.println(" _____ _ _____ _ _ \n" +
+ "/ __ \\ | | / ___(_) | | \n" +
+ "| / \\/ __ _ ___ ___ __ _ _ __ __| |_ __ __ _ \\ `--. _ __| | ___ ___ __ _ _ __ \n" +
+ "| | / _` / __/ __|/ _` | '_ \\ / _` | '__/ _` | `--. \\ |/ _` |/ _ \\/ __/ _` | '__|\n" +
+ "| \\__/\\ (_| \\__ \\__ \\ (_| | | | | (_| | | | (_| | /\\__/ / | (_| | __/ (_| (_| | | \n" +
+ " \\____/\\__,_|___/___/\\__,_|_| |_|\\__,_|_| \\__,_| \\____/|_|\\__,_|\\___|\\___\\__,_|_|\n" +
+ " \n" +
+ " ");
}
private void validate()
@@ -78,11 +83,15 @@
{
try
{
+ if (config.getKeyStorePath() == null || config.getKeystorePassword() == null)
+ throw new IllegalArgumentException("keyStorePath and keyStorePassword must be set if ssl enabled");
+
SslUtils.validateSslOpts(config.getKeyStorePath(), config.getKeystorePassword());
if (config.getTrustStorePath() != null && config.getTruststorePassword() != null)
SslUtils.validateSslOpts(config.getTrustStorePath(), config.getTruststorePassword());
- } catch (Exception e)
+ }
+ catch (Exception e)
{
throw new RuntimeException("Invalid keystore parameters for SSL", e);
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/Configuration.java b/src/main/java/org/apache/cassandra/sidecar/Configuration.java
index ed20ea0..5fff3b8 100644
--- a/src/main/java/org/apache/cassandra/sidecar/Configuration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/Configuration.java
@@ -18,6 +18,8 @@
package org.apache.cassandra.sidecar;
+import javax.annotation.Nullable;
+
/**
* Sidecar configuration
*/
@@ -39,27 +41,24 @@
private final Integer healthCheckFrequencyMillis;
/* SSL related settings */
+ @Nullable
private final String keyStorePath;
+ @Nullable
private final String keyStorePassword;
+ @Nullable
private final String trustStorePath;
+ @Nullable
private final String trustStorePassword;
private final boolean isSslEnabled;
- /**
- * Constructor
- *
- * @param cassandraHost
- * @param cassandraPort
- * @param port
- * @param healthCheckFrequencyMillis
- * @param trustStorePath
- * @param trustStorePassword
- */
public Configuration(String cassandraHost, Integer cassandraPort, String host, Integer port,
- Integer healthCheckFrequencyMillis, String keyStorePath, String keyStorePassword,
- String trustStorePath, String trustStorePassword, boolean isSslEnabled)
+ Integer healthCheckFrequencyMillis, boolean isSslEnabled,
+ @Nullable String keyStorePath,
+ @Nullable String keyStorePassword,
+ @Nullable String trustStorePath,
+ @Nullable String trustStorePassword)
{
this.cassandraHost = cassandraHost;
this.cassandraPort = cassandraPort;
@@ -139,6 +138,7 @@
*
* @return
*/
+ @Nullable
public String getKeyStorePath()
{
return keyStorePath;
@@ -149,6 +149,7 @@
*
* @return
*/
+ @Nullable
public String getKeystorePassword()
{
return keyStorePassword;
@@ -159,6 +160,7 @@
*
* @return
*/
+ @Nullable
public String getTrustStorePath()
{
return trustStorePath;
@@ -169,6 +171,7 @@
*
* @return
*/
+ @Nullable
public String getTruststorePassword()
{
return trustStorePassword;
@@ -252,8 +255,8 @@
public Configuration build()
{
- return new Configuration(cassandraHost, cassandraPort, host, port, healthCheckFrequencyMillis,
- keyStorePath, keyStorePassword, trustStorePath, trustStorePassword, isSslEnabled);
+ return new Configuration(cassandraHost, cassandraPort, host, port, healthCheckFrequencyMillis, isSslEnabled,
+ keyStorePath, keyStorePassword, trustStorePath, trustStorePassword);
}
}
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
index b161bac..08b66c0 100644
--- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java
+++ b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
@@ -18,6 +18,12 @@
package org.apache.cassandra.sidecar;
+import java.io.File;
+
+import org.apache.commons.configuration2.YAMLConfiguration;
+import org.apache.commons.configuration2.builder.fluent.Configurations;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
@@ -30,14 +36,11 @@
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.LoggerHandler;
import io.vertx.ext.web.handler.StaticHandler;
-import org.apache.cassandra.sidecar.routes.HealthCheck;
import org.apache.cassandra.sidecar.routes.HealthService;
-import org.apache.commons.configuration2.YAMLConfiguration;
-import org.apache.commons.configuration2.builder.fluent.Configurations;
-import org.apache.commons.configuration2.ex.ConfigurationException;
-import java.io.File;
-
+/**
+ * Provides main binding for more complex Guice dependencies
+ */
public class MainModule extends AbstractModule
{
@Provides
@@ -54,15 +57,7 @@
@Provides
@Singleton
- public HealthService healthService(Configuration config)
- {
- return new HealthService(config.getHealthCheckFrequencyMillis(),
- new HealthCheck(config.getCassandraHost(), config.getCassandraPort()));
- }
-
- @Provides
- @Singleton
- public HttpServer vertxServer(Vertx vertx, Router router, Configuration conf)
+ public HttpServer vertxServer(Vertx vertx, Configuration conf, Router router)
{
HttpServerOptions options = new HttpServerOptions().setLogActivity(true);
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/HealthCheck.java b/src/main/java/org/apache/cassandra/sidecar/routes/HealthCheck.java
index d2ac58e..46c57ea 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/HealthCheck.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/HealthCheck.java
@@ -18,38 +18,33 @@
package org.apache.cassandra.sidecar.routes;
-import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.Session;
-import com.datastax.driver.core.policies.RoundRobinPolicy;
-import com.datastax.driver.core.policies.WhiteListPolicy;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
-
-import java.net.InetSocketAddress;
-import java.util.Collections;
-import java.util.List;
import java.util.function.Supplier;
+import javax.annotation.Nullable;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.exceptions.NoHostAvailableException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.logging.Logger;
+import io.vertx.core.logging.LoggerFactory;
+import org.apache.cassandra.sidecar.CQLSession;
+
+/**
+ * Basic health check to verify that a CQL connection can be established with a basic SELECT query.
+ */
+@Singleton
public class HealthCheck implements Supplier<Boolean>
{
private static final Logger logger = LoggerFactory.getLogger(HealthCheck.class);
- private final String cassandraHost;
- private final int cassandraPort;
- private Cluster cluster;
- private Session session;
- /**
- * Constructor
- *
- * @param cassandraHost
- * @param cassandraPort
- */
- public HealthCheck(String cassandraHost, int cassandraPort)
+ @Nullable
+ private final CQLSession session;
+
+ @Inject
+ public HealthCheck(@Nullable CQLSession session)
{
- this.cassandraHost = cassandraHost;
- this.cassandraPort = cassandraPort;
- this.cluster = createCluster(cassandraHost, cassandraPort);
+ this.session = session;
}
/**
@@ -59,28 +54,25 @@
*/
private boolean check()
{
-
try
{
- if (cluster == null)
- cluster = createCluster(cassandraHost, cassandraPort);
-
- if (cluster == null)
- return false;
-
- if (session == null)
- session = cluster.connect();
-
- ResultSet rs = session.execute("SELECT release_version FROM system.local");
- return (rs.one() != null);
+ if (session != null && session.getLocalCql() != null)
+ {
+ ResultSet rs = session.getLocalCql().execute("SELECT release_version FROM system.local");
+ boolean result = (rs.one() != null);
+ logger.debug("HealthCheck status: {}", result);
+ return result;
+ }
+ }
+ catch (NoHostAvailableException nha)
+ {
+ logger.trace("NoHostAvailableException in HealthCheck - Cassandra Down");
}
catch (Exception e)
{
- logger.debug("Failed to reach Cassandra.", e);
- session = null;
- cluster = null;
- return false;
+ logger.error("Failed to reach Cassandra.", e);
}
+ return false;
}
/**
@@ -94,29 +86,4 @@
return check();
}
- /**
- * Creates a cluster object which ensures that the requests go only to the specified C* node
- *
- * @param cassandraHost
- * @param cassandraPort
- * @return
- */
- final private synchronized Cluster createCluster(String cassandraHost, int cassandraPort)
- {
- try
- {
- List<InetSocketAddress> wl = Collections.singletonList(InetSocketAddress.createUnresolved(cassandraHost, cassandraPort));
- cluster = Cluster.builder()
- .addContactPointsWithPorts(InetSocketAddress.createUnresolved(cassandraHost, cassandraPort))
- .withoutMetrics()
- .withLoadBalancingPolicy(new WhiteListPolicy(new RoundRobinPolicy(), wl))
- .build();
- }
- catch (Exception e)
- {
- logger.error("Failed to create Cluster object", e);
- }
-
- return cluster;
- }
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/HealthService.java b/src/main/java/org/apache/cassandra/sidecar/routes/HealthService.java
index 53903b2..ddf5c54 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/HealthService.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/HealthService.java
@@ -18,8 +18,19 @@
package org.apache.cassandra.sidecar.routes;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
import com.google.common.collect.ImmutableMap;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Host;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.http.HttpHeaders;
@@ -27,38 +38,47 @@
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.CQLSession;
+import org.apache.cassandra.sidecar.Configuration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Supplier;
-
-public class HealthService
+/**
+ * Tracks health check[s] and provides a REST response that should match that defined by api.yaml
+ */
+@Singleton
+public class HealthService implements Host.StateListener
{
private static final Logger logger = LoggerFactory.getLogger(HealthService.class);
- private final int CHECK_PERIOD_MS;
+ private final int checkPeriodMs;
private final Supplier<Boolean> check;
+ private volatile boolean registered = false;
+
+ @Nullable
+ private final CQLSession session;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private volatile boolean lastKnownStatus = false;
- public HealthService(int checkPeriodMillis, Supplier<Boolean> check)
+ @Inject
+ public HealthService(Configuration config, HealthCheck check, @Nullable CQLSession session)
{
- this.CHECK_PERIOD_MS = checkPeriodMillis;
+ this.checkPeriodMs = config.getHealthCheckFrequencyMillis();
+ this.session = session;
this.check = check;
}
- synchronized public void start()
+ public synchronized void start()
{
logger.info("Starting health check");
- executor.scheduleWithFixedDelay(this::refreshNow, 0, CHECK_PERIOD_MS, TimeUnit.MILLISECONDS);
+ maybeRegisterHostListener();
+ executor.scheduleWithFixedDelay(this::refreshNow, 0, checkPeriodMs, TimeUnit.MILLISECONDS);
}
- synchronized public void refreshNow()
+ public synchronized void refreshNow()
{
try
{
lastKnownStatus = this.check.get();
+ maybeRegisterHostListener();
}
catch (Exception e)
{
@@ -66,7 +86,19 @@
}
}
- synchronized public void stop()
+ private synchronized void maybeRegisterHostListener()
+ {
+ if (!registered)
+ {
+ if (session != null && session.getLocalCql() != null)
+ {
+ session.getLocalCql().getCluster().register(this);
+ registered = true;
+ }
+ }
+ }
+
+ public synchronized void stop()
{
logger.info("Stopping health check");
executor.shutdown();
@@ -76,9 +108,10 @@
{
try
{
+ int status = lastKnownStatus ? HttpResponseStatus.OK.code() : HttpResponseStatus.SERVICE_UNAVAILABLE.code();
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON)
- .setStatusCode(lastKnownStatus ? HttpResponseStatus.OK.code() : HttpResponseStatus.SERVICE_UNAVAILABLE.code())
+ .setStatusCode(status)
.end(Json.encode(ImmutableMap.of("status", lastKnownStatus ? "OK" : "NOT_OK")));
}
catch (Exception e)
@@ -87,4 +120,32 @@
rc.response().setStatusCode(400).end();
}
}
+
+ public void onAdd(Host host)
+ {
+ refreshNow();
+ }
+
+ public void onUp(Host host)
+ {
+ refreshNow();
+ }
+
+ public void onDown(Host host)
+ {
+ refreshNow();
+ }
+
+ public void onRemove(Host host)
+ {
+ refreshNow();
+ }
+
+ public void onRegister(Cluster cluster)
+ {
+ }
+
+ public void onUnregister(Cluster cluster)
+ {
+ }
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java b/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
index 8855f45..6efdd93 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
@@ -42,7 +42,8 @@
*/
public static void validateSslOpts(String keyStorePath, String keystorePassword) throws KeyStoreException,
NoSuchAlgorithmException,
- IOException, CertificateException
+ IOException,
+ CertificateException
{
final KeyStore ks;
@@ -53,7 +54,9 @@
else
throw new IllegalArgumentException("Unrecognized keystore format extension: "
+ keyStorePath.substring(keyStorePath.length() - 3));
-
- ks.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
+ try (FileInputStream keystore = new FileInputStream(keyStorePath))
+ {
+ ks.load(keystore, keystorePassword.toCharArray());
+ }
}
}
diff --git a/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java b/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
index 90f077f..48ed005 100644
--- a/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
@@ -35,7 +35,10 @@
import org.apache.cassandra.sidecar.mocks.MockHealthCheck;
import org.apache.cassandra.sidecar.routes.HealthService;
-abstract public class AbstractHealthServiceTest
+/**
+ * Provides basic tests shared between SSL and normal http health services
+ */
+public abstract class AbstractHealthServiceTest
{
private MockHealthCheck check;
private HealthService service;
@@ -77,7 +80,8 @@
client.get(config.getPort(), "localhost", "/api/v1/__health")
.as(BodyCodec.string())
.ssl(isSslEnabled())
- .send(testContext.succeeding(response -> testContext.verify(() -> {
+ .send(testContext.succeeding(response -> testContext.verify(() ->
+ {
Assert.assertEquals(200, response.statusCode());
testContext.completeNow();
})));
@@ -95,7 +99,8 @@
client.get(config.getPort(), "localhost", "/api/v1/__health")
.as(BodyCodec.string())
.ssl(isSslEnabled())
- .send(testContext.succeeding(response -> testContext.verify(() -> {
+ .send(testContext.succeeding(response -> testContext.verify(() ->
+ {
Assert.assertEquals(503, response.statusCode());
testContext.completeNow();
})));
diff --git a/src/test/java/org/apache/cassandra/sidecar/HealthServiceSslTest.java b/src/test/java/org/apache/cassandra/sidecar/HealthServiceSslTest.java
index d89d861..9ceb2b8 100644
--- a/src/test/java/org/apache/cassandra/sidecar/HealthServiceSslTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/HealthServiceSslTest.java
@@ -25,6 +25,9 @@
import io.vertx.core.Vertx;
import io.vertx.junit5.VertxExtension;
+/**
+ * Health Service SSL Tests
+ */
@DisplayName("Health Service SSL Test")
@ExtendWith(VertxExtension.class)
public class HealthServiceSslTest extends AbstractHealthServiceTest
diff --git a/src/test/java/org/apache/cassandra/sidecar/HealthServiceTest.java b/src/test/java/org/apache/cassandra/sidecar/HealthServiceTest.java
index 67a6220..2528660 100644
--- a/src/test/java/org/apache/cassandra/sidecar/HealthServiceTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/HealthServiceTest.java
@@ -25,6 +25,9 @@
import io.vertx.core.Vertx;
import io.vertx.junit5.VertxExtension;
+/**
+ * Health Service Tests
+ */
@DisplayName("Health Service Test")
@ExtendWith(VertxExtension.class)
public class HealthServiceTest extends AbstractHealthServiceTest
diff --git a/src/test/java/org/apache/cassandra/sidecar/TestModule.java b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index 0bb17ad..102c95c 100644
--- a/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -30,6 +30,9 @@
import org.apache.cassandra.sidecar.mocks.MockHealthCheck;
import org.apache.cassandra.sidecar.routes.HealthService;
+/**
+ * Provides the basic dependencies for unit tests.
+ */
public class TestModule extends AbstractModule
{
private Vertx vertx;
@@ -50,7 +53,7 @@
@Singleton
public HealthService healthService(Configuration config, MockHealthCheck check)
{
- return new HealthService(config.getHealthCheckFrequencyMillis(), check);
+ return new HealthService(config, check, null);
}
@Provides
diff --git a/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java b/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
index 078327f..0e9cd9b 100644
--- a/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
+++ b/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
@@ -20,6 +20,9 @@
import io.vertx.core.Vertx;
+/**
+ * Changes to the TestModule to define SSL dependencies
+ */
public class TestSslModule extends TestModule
{
public TestSslModule(Vertx vertx)
diff --git a/src/test/java/org/apache/cassandra/sidecar/mocks/MockHealthCheck.java b/src/test/java/org/apache/cassandra/sidecar/mocks/MockHealthCheck.java
index 76a7280..c37fef1 100644
--- a/src/test/java/org/apache/cassandra/sidecar/mocks/MockHealthCheck.java
+++ b/src/test/java/org/apache/cassandra/sidecar/mocks/MockHealthCheck.java
@@ -18,13 +18,20 @@
package org.apache.cassandra.sidecar.mocks;
-import java.util.function.Supplier;
+import org.apache.cassandra.sidecar.routes.HealthCheck;
-public class MockHealthCheck implements Supplier<Boolean>
+/**
+ * Settable HealthCheck
+ */
+public class MockHealthCheck extends HealthCheck
{
private volatile boolean status;
- @Override
+ public MockHealthCheck()
+ {
+ super(null);
+ }
+
public Boolean get()
{
return status;