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">-->
+    <!--&lt;!&ndash; Checks that each Java package has a Javadoc file used for commenting.-->
+    <!--Only allows a package-info.java, not package.html. &ndash;&gt;-->
+    <!--<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;