CASSANDRASC-70: Added Client Methods for Obtaining Sidecar and Cassandra Health
Patch by Yuriy Semchyshyn; Reviewed by Dinesh Joshi, Francisco Guerrero, Yifan Cai for CASSANDRASC-70
diff --git a/CHANGES.txt b/CHANGES.txt
index 5453a0e..64ecc54 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
1.0.0
-----
+ * Add Client Methods for Obtaining Sidecar and Cassandra Health (CASSANDRASC-70)
* Publish bytes streamed and written metrics (CASSANDRASC-68)
* Extract the in-jvm dtest template for use in other projects (CASSANDRASC-55)
* Fix relocation of native libraries for vertx-client-shaded (CASSANDRASC-67)
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
index 0c3a16e..8d09d17 100644
--- a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
@@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.Map;
+import org.apache.cassandra.sidecar.client.request.CassandraHealthRequest;
import org.apache.cassandra.sidecar.client.request.CleanSSTableUploadSessionRequest;
import org.apache.cassandra.sidecar.client.request.ClearSnapshotRequest;
import org.apache.cassandra.sidecar.client.request.CreateSnapshotRequest;
@@ -33,6 +34,7 @@
import org.apache.cassandra.sidecar.client.request.RingRequest;
import org.apache.cassandra.sidecar.client.request.SSTableComponentRequest;
import org.apache.cassandra.sidecar.client.request.SchemaRequest;
+import org.apache.cassandra.sidecar.client.request.SidecarHealthRequest;
import org.apache.cassandra.sidecar.client.request.TimeSkewRequest;
import org.apache.cassandra.sidecar.client.request.UploadSSTableRequest;
import org.apache.cassandra.sidecar.client.retry.ExponentialBackoffRetryPolicy;
@@ -48,6 +50,8 @@
*/
public class RequestContext
{
+ protected static final SidecarHealthRequest SIDECAR_HEALTH_REQUEST = new SidecarHealthRequest();
+ protected static final CassandraHealthRequest CASSANDRA_HEALTH_REQUEST = new CassandraHealthRequest();
protected static final SchemaRequest FULL_SCHEMA_REQUEST = new SchemaRequest();
protected static final TimeSkewRequest TIME_SKEW_REQUEST = new TimeSkewRequest();
protected static final NodeSettingsRequest NODE_SETTINGS_REQUEST = new NodeSettingsRequest();
@@ -174,6 +178,28 @@
}
/**
+ * Sets the {@code request} to be a {@link SidecarHealthRequest}
+ * and returns a reference to this Builder enabling method chaining
+ *
+ * @return a reference to this Builder
+ */
+ public Builder sidecarHealthRequest()
+ {
+ return request(SIDECAR_HEALTH_REQUEST);
+ }
+
+ /**
+ * Sets the {@code request} to be a {@link CassandraHealthRequest}
+ * and returns a reference to this Builder enabling method chaining
+ *
+ * @return a reference to this Builder
+ */
+ public Builder cassandraHealthRequest()
+ {
+ return request(CASSANDRA_HEALTH_REQUEST);
+ }
+
+ /**
* Sets the {@code request} to be a {@link SchemaRequest} for the full schema and returns a reference to
* this Builder enabling method chaining.
*
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index 450dc5f..16b50c2 100644
--- a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -28,12 +28,14 @@
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.cassandra.sidecar.client.request.ImportSSTableRequest;
import org.apache.cassandra.sidecar.client.retry.IgnoreConflictRetryPolicy;
+import org.apache.cassandra.sidecar.client.retry.OncePerInstanceRetryPolicy;
import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
import org.apache.cassandra.sidecar.client.retry.RunnableOnStatusCodeRetryPolicy;
import org.apache.cassandra.sidecar.client.selection.InstanceSelectionPolicy;
import org.apache.cassandra.sidecar.client.selection.RandomInstanceSelectionPolicy;
import org.apache.cassandra.sidecar.common.NodeSettings;
import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
+import org.apache.cassandra.sidecar.common.data.HealthResponse;
import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
import org.apache.cassandra.sidecar.common.data.RingResponse;
import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
@@ -69,6 +71,32 @@
}
/**
+ * Executes the Sidecar health request using the configured selection policy and with no retries
+ *
+ * @return a completable future of the Sidecar health response
+ */
+ public CompletableFuture<HealthResponse> sidecarHealth()
+ {
+ return executor.executeRequestAsync(requestBuilder()
+ .sidecarHealthRequest()
+ .retryPolicy(new OncePerInstanceRetryPolicy())
+ .build());
+ }
+
+ /**
+ * Executes the Cassandra health request using the configured selection policy and with no retries
+ *
+ * @return a completable future of the Cassandra health response
+ */
+ public CompletableFuture<HealthResponse> cassandraHealth()
+ {
+ return executor.executeRequestAsync(requestBuilder()
+ .cassandraHealthRequest()
+ .retryPolicy(new OncePerInstanceRetryPolicy())
+ .build());
+ }
+
+ /**
* Executes the full schema request using the default retry policy and configured selection policy
*
* @return a completable future of the full schema response
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/request/CassandraHealthRequest.java b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CassandraHealthRequest.java
new file mode 100644
index 0000000..2061c89
--- /dev/null
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CassandraHealthRequest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.client.request;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.HealthResponse;
+
+/**
+ * Represents a request to retrieve the Cassandra health
+ */
+public class CassandraHealthRequest extends DecodableRequest<HealthResponse>
+{
+ /**
+ * Constructs a request to retrieve the Cassandra health
+ */
+ public CassandraHealthRequest()
+ {
+ super(ApiEndpointsV1.CASSANDRA_HEALTH_ROUTE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.GET;
+ }
+}
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/request/SidecarHealthRequest.java b/client/src/main/java/org/apache/cassandra/sidecar/client/request/SidecarHealthRequest.java
new file mode 100644
index 0000000..49aa139
--- /dev/null
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/request/SidecarHealthRequest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.client.request;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.HealthResponse;
+
+/**
+ * Represents a request to retrieve the Sidecar health
+ */
+public class SidecarHealthRequest extends DecodableRequest<HealthResponse>
+{
+ /**
+ * Constructs a request to retrieve the Sidecar health
+ */
+ public SidecarHealthRequest()
+ {
+ super(ApiEndpointsV1.HEALTH_ROUTE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.GET;
+ }
+}
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/retry/OncePerInstanceRetryPolicy.java b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/OncePerInstanceRetryPolicy.java
new file mode 100644
index 0000000..cb14429
--- /dev/null
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/OncePerInstanceRetryPolicy.java
@@ -0,0 +1,59 @@
+/*
+ * 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.client.retry;
+
+import java.util.concurrent.CompletableFuture;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.apache.cassandra.sidecar.client.HttpResponse;
+import org.apache.cassandra.sidecar.client.exception.RetriesExhaustedException;
+import org.apache.cassandra.sidecar.client.request.Request;
+
+/**
+ * A retry policy that attempts to execute the request once on each instance
+ * until the first successful response is received, or fails if none were successful
+ */
+public class OncePerInstanceRetryPolicy extends RetryPolicy
+{
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onResponse(CompletableFuture<HttpResponse> responseFuture,
+ Request request,
+ HttpResponse response,
+ Throwable throwable,
+ int attempts,
+ boolean canRetryOnADifferentHost,
+ RetryAction retryAction)
+ {
+ if (response != null && response.statusCode() == HttpResponseStatus.OK.code())
+ {
+ responseFuture.complete(response);
+ }
+ else if (canRetryOnADifferentHost)
+ {
+ retryAction.retry(attempts + 1, 0L);
+ }
+ else
+ {
+ responseFuture.completeExceptionally(RetriesExhaustedException.of(attempts, request, response, throwable));
+ }
+ }
+}
diff --git a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index 77bc69a..68eb1b3 100644
--- a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++ b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -58,6 +58,7 @@
import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
import org.apache.cassandra.sidecar.common.NodeSettings;
import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
+import org.apache.cassandra.sidecar.common.data.HealthResponse;
import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
import org.apache.cassandra.sidecar.common.data.RingEntry;
import org.apache.cassandra.sidecar.common.data.RingResponse;
@@ -73,6 +74,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
abstract class SidecarClientTest
@@ -109,6 +111,72 @@
}
@Test
+ void testSidecarHealthOk() throws Exception
+ {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody("{\"status\":\"OK\"}");
+ enqueue(response);
+
+ HealthResponse result = client.sidecarHealth().get(30, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result.status()).isEqualToIgnoringCase("OK");
+ assertThat(result.isOk()).isTrue();
+
+ validateResponseServed(ApiEndpointsV1.HEALTH_ROUTE);
+ }
+
+ @Test
+ void testSidecarHealthNotOk() throws Exception
+ {
+ MockResponse response = new MockResponse()
+ .setResponseCode(503)
+ .setHeader("content-type", "application/json")
+ .setBody("{\"status\":\"NOT_OK\"}");
+ enqueue(response);
+
+ assertThatThrownBy(() -> client.sidecarHealth().get(30, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCauseInstanceOf(RetriesExhaustedException.class);
+
+ validateResponseServed(ApiEndpointsV1.HEALTH_ROUTE);
+ }
+
+ @Test
+ void testCassandraHealthOk() throws Exception
+ {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody("{\"status\":\"OK\"}");
+ enqueue(response);
+
+ HealthResponse result = client.cassandraHealth().get(30, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result.status()).isEqualToIgnoringCase("OK");
+ assertThat(result.isOk()).isTrue();
+
+ validateResponseServed(ApiEndpointsV1.CASSANDRA_HEALTH_ROUTE);
+ }
+
+ @Test
+ void testCassandraHealthNotOk() throws Exception
+ {
+ MockResponse response = new MockResponse()
+ .setResponseCode(503)
+ .setHeader("content-type", "application/json")
+ .setBody("{\"status\":\"NOT_OK\"}");
+ enqueue(response);
+
+ assertThatThrownBy(() -> client.cassandraHealth().get(30, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCauseInstanceOf(RetriesExhaustedException.class);
+
+ validateResponseServed(ApiEndpointsV1.CASSANDRA_HEALTH_ROUTE);
+ }
+
+ @Test
void testFullSchema() throws Exception
{
String fullSchemaAsString = "{\"schema\":\"CREATE KEYSPACE sample_ks.sample_table ...\"}";
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/HealthResponse.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/HealthResponse.java
new file mode 100644
index 0000000..0869671
--- /dev/null
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/HealthResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.data;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A class representing a response for a health request
+ * (either {@code SidecarHealthRequest} or {@code CassandraHealthRequest})
+ */
+public class HealthResponse
+{
+ @NotNull
+ private final String status;
+
+ /**
+ * Constructs a {@link HealthResponse} object with the given {@code status}
+ *
+ * @param status the status
+ */
+ public HealthResponse(@NotNull @JsonProperty("status") String status)
+ {
+ this.status = Objects.requireNonNull(status, "status must not be null").toUpperCase();
+ }
+
+ /**
+ * @return the status
+ */
+ @JsonProperty("status")
+ public String status()
+ {
+ return status;
+ }
+
+ /**
+ * @return whether the status is OK
+ */
+ @JsonIgnore
+ public boolean isOk()
+ {
+ return status.equals("OK");
+ }
+}