You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by yc...@apache.org on 2022/10/17 23:27:10 UTC
[cassandra-sidecar] branch trunk updated: CASSANDRASC-43 Add Schema API
This is an automated email from the ASF dual-hosted git repository.
ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new 6e358ac CASSANDRASC-43 Add Schema API
6e358ac is described below
commit 6e358acfce071cad16ac88c15dc2229bbb8a7944
Author: Francisco Guerrero <fr...@apple.com>
AuthorDate: Fri Oct 7 16:39:12 2022 -0700
CASSANDRASC-43 Add Schema API
This commit introduces the Schema API. Two new endpoints are added:
- /api/v1/schema/keyspaces
This endpoint returns the SchemaResponse with the full schema for all keyspaces.
- /api/v1/schema/keyspaces/:keyspace
This endpoint returns the SchemaResponse with the full schema for the requested keyspace.
patch by Francisco Guerrero; reviewed by Yifan Cai, Dinesh Joshi for CASSANDRASC-43
---
build.gradle | 1 +
checkstyle.xml | 6 +-
.../common/data/ListSnapshotFilesRequest.java | 6 +-
.../sidecar/common/data/QualifiedTableName.java | 4 +-
.../sidecar/common/data/SSTableComponent.java | 6 +-
.../{SSTableComponent.java => SchemaRequest.java} | 37 ++--
.../sidecar/common/data/SchemaResponse.java | 75 ++++++++
.../common/data/StreamSSTableComponentRequest.java | 8 +-
.../common/data/ListSnapshotFilesRequestTest.java | 6 +-
.../data/StreamSSTableComponentRequestTest.java | 8 +-
.../org/apache/cassandra/sidecar/MainModule.java | 10 +
.../org/apache/cassandra/sidecar/models/Range.java | 6 +-
.../sidecar/routes/ListSnapshotFilesHandler.java | 4 +-
.../cassandra/sidecar/routes/SchemaHandler.java | 159 ++++++++++++++++
.../sidecar/snapshots/SnapshotPathBuilder.java | 16 +-
.../cassandra/sidecar/utils/FileStreamer.java | 2 +-
.../sidecar/routes/SchemaHandlerTest.java | 201 +++++++++++++++++++++
.../routes/StreamSSTableComponentHandlerTest.java | 4 +-
src/test/resources/schema/test_keyspace_schema.cql | 63 +++++++
19 files changed, 562 insertions(+), 60 deletions(-)
diff --git a/build.gradle b/build.gradle
index 87cc251..baa6168 100644
--- a/build.gradle
+++ b/build.gradle
@@ -113,6 +113,7 @@ dependencies {
implementation('io.swagger.core.v3:swagger-jaxrs2:2.1.0')
implementation("org.jboss.resteasy:resteasy-vertx:4.7.4.Final")
implementation(group: 'org.jboss.spec.javax.servlet', name: 'jboss-servlet-api_4.0_spec', version: '2.0.0.Final')
+ implementation('org.jetbrains:annotations:23.0.0')
// Trying to be exactly compatible with Cassandra's deps
implementation('org.slf4j:slf4j-api:1.7.25')
diff --git a/checkstyle.xml b/checkstyle.xml
index 6206060..4108268 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -260,6 +260,10 @@ page at http://checkstyle.sourceforge.net/config.html -->
<!-- Checks for placement of the left curly brace ('{'). -->
<property name="option" value="nl"/>
<property name="severity" value="error" />
+ <property name="tokens" value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF,
+ ENUM_DEF, INTERFACE_DEF, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
+ LITERAL_FOR, LITERAL_IF, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE,
+ METHOD_DEF, OBJBLOCK, STATIC_INIT"/>
</module>
<module name="RightCurly">
@@ -390,4 +394,4 @@ page at http://checkstyle.sourceforge.net/config.html -->
<property name="messageFormat" value="$1" />
</module>
-</module>
\ No newline at end of file
+</module>
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequest.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequest.java
index 887ad26..ec121ca 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequest.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequest.java
@@ -47,7 +47,7 @@ public class ListSnapshotFilesRequest extends QualifiedTableName
/**
* @return the name of the snapshot
*/
- public String getSnapshotName()
+ public String snapshotName()
{
return snapshotName;
}
@@ -66,8 +66,8 @@ public class ListSnapshotFilesRequest extends QualifiedTableName
public String toString()
{
return "ListSnapshotFilesRequest{" +
- "keyspace='" + getKeyspace() + '\'' +
- ", tableName='" + getTableName() + '\'' +
+ "keyspace='" + keyspace() + '\'' +
+ ", tableName='" + tableName() + '\'' +
", snapshotName='" + snapshotName + '\'' +
", includeSecondaryIndexFiles=" + includeSecondaryIndexFiles +
'}';
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/QualifiedTableName.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/QualifiedTableName.java
index 9bbcb39..78d3a79 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/data/QualifiedTableName.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/QualifiedTableName.java
@@ -59,7 +59,7 @@ public class QualifiedTableName
/**
* @return the keyspace in Cassandra
*/
- public String getKeyspace()
+ public String keyspace()
{
return keyspace;
}
@@ -67,7 +67,7 @@ public class QualifiedTableName
/**
* @return the table name in Cassandra
*/
- public String getTableName()
+ public String tableName()
{
return tableName;
}
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java
index 1752d2c..9bb039b 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java
@@ -41,7 +41,7 @@ public class SSTableComponent extends QualifiedTableName
/**
* @return the name of the SSTable component
*/
- public String getComponentName()
+ public String componentName()
{
return componentName;
}
@@ -53,8 +53,8 @@ public class SSTableComponent extends QualifiedTableName
public String toString()
{
return "SSTableComponent{" +
- "keyspace='" + getKeyspace() + '\'' +
- ", tableName='" + getTableName() + '\'' +
+ "keyspace='" + keyspace() + '\'' +
+ ", tableName='" + tableName() + '\'' +
", componentName='" + componentName + '\'' +
'}';
}
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaRequest.java
similarity index 50%
copy from common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java
copy to common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaRequest.java
index 1752d2c..1f136b9 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/data/SSTableComponent.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaRequest.java
@@ -15,47 +15,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.cassandra.sidecar.common.data;
+import org.jetbrains.annotations.Nullable;
+
/**
- * Represents an SSTable component that includes a keyspace, table name and component name
+ * Holder class for the {@link org.apache.cassandra.sidecar.routes.SchemaHandler}
+ * request parameters
*/
-public class SSTableComponent extends QualifiedTableName
+public class SchemaRequest extends QualifiedTableName
{
- private final String componentName;
-
/**
- * Constructor for the holder class
+ * Constructs a {@link SchemaRequest} with the {@link org.jetbrains.annotations.Nullable} {@code keyspace}.
*
- * @param keyspace the keyspace in Cassandra
- * @param tableName the table name in Cassandra
- * @param componentName the name of the SSTable component
- */
- public SSTableComponent(String keyspace, String tableName, String componentName)
- {
- super(keyspace, tableName);
- this.componentName = validator.validateComponentName(componentName);
- }
-
- /**
- * @return the name of the SSTable component
+ * @param keyspace the keyspace in Cassandra
*/
- public String getComponentName()
+ public SchemaRequest(@Nullable String keyspace)
{
- return componentName;
+ super(keyspace, null, false);
}
/**
* {@inheritDoc}
*/
- @Override
public String toString()
{
- return "SSTableComponent{" +
- "keyspace='" + getKeyspace() + '\'' +
- ", tableName='" + getTableName() + '\'' +
- ", componentName='" + componentName + '\'' +
+ return "SchemaRequest{" +
+ "keyspace='" + keyspace() + '\'' +
+ ", tableName='" + tableName() + '\'' +
'}';
}
}
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaResponse.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaResponse.java
new file mode 100644
index 0000000..a9efb2b
--- /dev/null
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/SchemaResponse.java
@@ -0,0 +1,75 @@
+/*
+ * 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.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A class representing a response for the {@link SchemaRequest}.
+ */
+public class SchemaResponse
+{
+ private final String keyspace;
+ private final String schema;
+
+ /**
+ * Constructs a {@link SchemaResponse} object with the given {@code schema}.
+ *
+ * @param schema the schema for all keyspaces
+ */
+ public SchemaResponse(String schema)
+ {
+ this.keyspace = null;
+ this.schema = Objects.requireNonNull(schema, "schema must be non-null");
+ }
+
+ /**
+ * Constructs a {@link SchemaResponse} object with the {@code schema} for the given {@code keyspace}.
+ *
+ * @param keyspace the keyspace in Cassandra
+ * @param schema the schema for the given {@code keyspace}
+ */
+ public SchemaResponse(@JsonProperty("keyspace") String keyspace,
+ @JsonProperty("schema") String schema)
+ {
+ this.keyspace = Objects.requireNonNull(keyspace, "keyspace must be non-null");
+ this.schema = Objects.requireNonNull(schema, "schema must be non-null");
+ }
+
+ /**
+ * @return the name of the Cassandra keyspace
+ */
+ @JsonProperty("keyspace")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public String keyspace()
+ {
+ return keyspace;
+ }
+
+ /**
+ * @return the string representing the schema for the response
+ */
+ @JsonProperty("schema")
+ public String schema()
+ {
+ return schema;
+ }
+}
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequest.java b/common/src/main/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequest.java
index b72c9f2..0d2c61c 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequest.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequest.java
@@ -43,7 +43,7 @@ public class StreamSSTableComponentRequest extends SSTableComponent
/**
* @return the name of the snapshot
*/
- public String getSnapshotName()
+ public String snapshotName()
{
return snapshotName;
}
@@ -54,10 +54,10 @@ public class StreamSSTableComponentRequest extends SSTableComponent
public String toString()
{
return "StreamSSTableComponentRequest{" +
- "keyspace='" + getKeyspace() + '\'' +
- ", tableName='" + getTableName() + '\'' +
+ "keyspace='" + keyspace() + '\'' +
+ ", tableName='" + tableName() + '\'' +
", snapshot='" + snapshotName + '\'' +
- ", componentName='" + getComponentName() + '\'' +
+ ", componentName='" + componentName() + '\'' +
'}';
}
}
diff --git a/common/src/test/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequestTest.java b/common/src/test/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequestTest.java
index f00f070..8bc7570 100644
--- a/common/src/test/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequestTest.java
+++ b/common/src/test/java/org/apache/cassandra/sidecar/common/data/ListSnapshotFilesRequestTest.java
@@ -142,9 +142,9 @@ class ListSnapshotFilesRequestTest
{
ListSnapshotFilesRequest request = new ListSnapshotFilesRequest("ks", "table", "snapshot", false);
- assertThat(request.getKeyspace()).isEqualTo("ks");
- assertThat(request.getTableName()).isEqualTo("table");
- assertThat(request.getSnapshotName()).isEqualTo("snapshot");
+ assertThat(request.keyspace()).isEqualTo("ks");
+ assertThat(request.tableName()).isEqualTo("table");
+ assertThat(request.snapshotName()).isEqualTo("snapshot");
assertThat(request.includeSecondaryIndexFiles()).isFalse();
assertThat(request.toString()).isEqualTo("ListSnapshotFilesRequest{keyspace='ks', tableName='table', " +
"snapshotName='snapshot', includeSecondaryIndexFiles=false}");
diff --git a/common/src/test/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequestTest.java b/common/src/test/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequestTest.java
index 5346abb..ee500dd 100644
--- a/common/src/test/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequestTest.java
+++ b/common/src/test/java/org/apache/cassandra/sidecar/common/data/StreamSSTableComponentRequestTest.java
@@ -164,10 +164,10 @@ class StreamSSTableComponentRequestTest
StreamSSTableComponentRequest req =
new StreamSSTableComponentRequest("ks", "table", "snapshot", "data.db");
- assertThat(req.getKeyspace()).isEqualTo("ks");
- assertThat(req.getTableName()).isEqualTo("table");
- assertThat(req.getSnapshotName()).isEqualTo("snapshot");
- assertThat(req.getComponentName()).isEqualTo("data.db");
+ assertThat(req.keyspace()).isEqualTo("ks");
+ assertThat(req.tableName()).isEqualTo("table");
+ assertThat(req.snapshotName()).isEqualTo("snapshot");
+ assertThat(req.componentName()).isEqualTo("data.db");
assertThat(req.toString()).isEqualTo("StreamSSTableComponentRequest{keyspace='ks', tableName='table', " +
"snapshot='snapshot', componentName='data.db'}");
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
index a121457..3ef46b3 100644
--- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java
+++ b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
@@ -65,6 +65,7 @@ import org.apache.cassandra.sidecar.routes.CassandraHealthService;
import org.apache.cassandra.sidecar.routes.FileStreamHandler;
import org.apache.cassandra.sidecar.routes.HealthService;
import org.apache.cassandra.sidecar.routes.ListSnapshotFilesHandler;
+import org.apache.cassandra.sidecar.routes.SchemaHandler;
import org.apache.cassandra.sidecar.routes.StreamSSTableComponentHandler;
import org.apache.cassandra.sidecar.routes.SwaggerOpenApiResource;
import org.jboss.resteasy.plugins.server.vertx.VertxRegistry;
@@ -168,6 +169,7 @@ public class MainModule extends AbstractModule
StreamSSTableComponentHandler streamSSTableComponentHandler,
FileStreamHandler fileStreamHandler,
ListSnapshotFilesHandler listSnapshotFilesHandler,
+ SchemaHandler schemaHandler,
LoggerHandler loggerHandler,
ErrorHandler errorHandler)
{
@@ -194,6 +196,14 @@ public class MainModule extends AbstractModule
router.get(API_V1_VERSION + listSnapshotFilesRoute)
.handler(listSnapshotFilesHandler);
+ final String allKeyspacesSchemasRoute = "/schema/keyspaces";
+ router.get(API_V1_VERSION + allKeyspacesSchemasRoute)
+ .handler(schemaHandler);
+
+ final String keyspaceSchemaRoute = "/schema/keyspaces/:keyspace";
+ router.get(API_V1_VERSION + keyspaceSchemaRoute)
+ .handler(schemaHandler);
+
return router;
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/models/Range.java b/src/main/java/org/apache/cassandra/sidecar/models/Range.java
index e224a9c..2def7fc 100644
--- a/src/main/java/org/apache/cassandra/sidecar/models/Range.java
+++ b/src/main/java/org/apache/cassandra/sidecar/models/Range.java
@@ -21,11 +21,11 @@ package org.apache.cassandra.sidecar.models;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import javax.validation.constraints.NotNull;
import com.google.common.base.Preconditions;
import org.apache.cassandra.sidecar.exceptions.RangeException;
+import org.jetbrains.annotations.NotNull;
/**
* Accepted Range formats are start-end, start-, -suffix_length
@@ -116,8 +116,8 @@ public class Range
private static IllegalArgumentException invalidRangeHeaderException(String rangeHeader)
{
return new IllegalArgumentException("Invalid range header: " + rangeHeader + ". " +
- "Supported Range formats are bytes=<start>-<end>, bytes=<start>-, " +
- "bytes=-<suffix-length>");
+ "Supported Range formats are bytes=<start>-<end>, " +
+ "bytes=<start>-, bytes=-<suffix-length>");
}
// An initialized range is always valid; invalid params fail range initialization.
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/ListSnapshotFilesHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/ListSnapshotFilesHandler.java
index 5fe8502..dd89042 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/ListSnapshotFilesHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/ListSnapshotFilesHandler.java
@@ -27,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.net.SocketAddress;
@@ -51,6 +52,7 @@ import org.apache.cassandra.sidecar.snapshots.SnapshotPathBuilder;
* lists all SSTable component files including secondary index files for the "testSnapshot" snapshot for the "ks"
* keyspace and the "tbl" table
*/
+@Singleton
public class ListSnapshotFilesHandler extends AbstractHandler
{
private static final Logger logger = LoggerFactory.getLogger(ListSnapshotFilesHandler.class);
@@ -85,7 +87,7 @@ public class ListSnapshotFilesHandler extends AbstractHandler
{
if (fileList.isEmpty())
{
- String payload = "Snapshot '" + requestParams.getSnapshotName() +
+ String payload = "Snapshot '" + requestParams.snapshotName() +
"' not found";
context.fail(new HttpException(HttpResponseStatus.NOT_FOUND.code(), payload));
}
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java
new file mode 100644
index 0000000..0a3fe3f
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java
@@ -0,0 +1,159 @@
+/*
+ * 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.routes;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.Session;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.cluster.InstancesConfig;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.data.SchemaRequest;
+import org.apache.cassandra.sidecar.common.data.SchemaResponse;
+
+
+/**
+ * The {@link SchemaHandler} class returns a
+ */
+@Singleton
+public class SchemaHandler extends AbstractHandler
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(SchemaHandler.class);
+
+ /**
+ * Constructs a handler with the provided {@code instancesConfig}
+ *
+ * @param instancesConfig the instances configuration
+ */
+ @Inject
+ protected SchemaHandler(InstancesConfig instancesConfig)
+ {
+ super(instancesConfig);
+ }
+
+ @Override
+ public void handle(RoutingContext context)
+ {
+ final HttpServerRequest request = context.request();
+ final String host = getHost(context);
+ final SocketAddress remoteAddress = request.remoteAddress();
+ final SchemaRequest requestParams = extractParamsOrThrow(context);
+ final InstanceMetadata instanceMeta = instancesConfig.instanceFromHost(host);
+ LOGGER.debug("SchemaHandler received request: {} from: {}. Instance: {}",
+ requestParams, remoteAddress, host);
+
+ final Vertx vertx = context.vertx();
+ getMetadata(vertx, instanceMeta).onFailure(throwable -> handleFailure(context, requestParams, throwable))
+ .onSuccess(metadata -> handleWithMetadata(context, requestParams, metadata));
+ }
+
+ /**
+ * Handles the request with the Cassandra {@link Metadata metadata}.
+ *
+ * @param context the event to handle
+ * @param requestParams the {@link SchemaRequest} parsed from the request
+ * @param metadata the metadata on the connected cluster, including known nodes and schema definitions
+ */
+ private void handleWithMetadata(RoutingContext context, SchemaRequest requestParams, Metadata metadata)
+ {
+ if (metadata == null)
+ {
+ // set request as failed and return
+ LOGGER.error("Failed to obtain metadata on the connected cluster for request '{}'", requestParams);
+ context.fail(HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
+ return;
+ }
+
+ if (requestParams.keyspace() == null)
+ {
+ SchemaResponse schemaResponse = new SchemaResponse(metadata.exportSchemaAsString());
+ context.json(schemaResponse);
+ return;
+ }
+
+ // retrieve keyspace metadata
+ KeyspaceMetadata ksMetadata = metadata.getKeyspace(requestParams.keyspace());
+ if (ksMetadata == null)
+ {
+ // set request as failed and return
+ // keyspace does not exist
+ String errorMessage = String.format("Keyspace '%s' does not exist.",
+ requestParams.keyspace());
+ context.fail(new HttpException(HttpResponseStatus.NOT_FOUND.code(), errorMessage));
+ return;
+ }
+
+ SchemaResponse schemaResponse = new SchemaResponse(requestParams.keyspace(), ksMetadata.exportAsString());
+ context.json(schemaResponse);
+ }
+
+ private void handleFailure(RoutingContext context, SchemaRequest requestParams, Throwable throwable)
+ {
+ LOGGER.error("Failed to obtain keyspace metadata for request '{}'", requestParams, throwable);
+ context.fail(new HttpException(HttpResponseStatus.SERVICE_UNAVAILABLE.code(),
+ "Unable to reach the Cassandra service", throwable));
+ }
+
+ /**
+ * Gets cluster metadata asynchronously.
+ *
+ * @param vertx the vertx instance
+ * @param instanceMetadata the instance metadata
+ * @return {@link Future} containing {@link Metadata}
+ */
+ private Future<Metadata> getMetadata(Vertx vertx, InstanceMetadata instanceMetadata)
+ {
+ return vertx.executeBlocking(promise -> {
+ // session() or getLocalCql() can potentially block, so move them inside a executeBlocking lambda
+ Session session = instanceMetadata.session().getLocalCql();
+ if (session == null)
+ {
+ LOGGER.error("Unable to obtain session for instance='{}' host='{}' port='{}'",
+ instanceMetadata.id(), instanceMetadata.host(), instanceMetadata.port());
+ promise.fail(new RuntimeException(String.format("Could not obtain session for instance '%d'",
+ instanceMetadata.id())));
+ }
+ else
+ {
+ promise.complete(session.getCluster().getMetadata());
+ }
+ });
+ }
+
+ /**
+ * Parses the request parameters
+ *
+ * @param rc the event to handle
+ * @return the {@link SchemaRequest} parsed from the request
+ */
+ private SchemaRequest extractParamsOrThrow(final RoutingContext rc)
+ {
+ return new SchemaRequest(rc.pathParam("keyspace"));
+ }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java b/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
index 528a1a3..de9538d 100644
--- a/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
+++ b/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
@@ -98,10 +98,10 @@ public class SnapshotPathBuilder
validate(request);
// Search for the file
return getDataDirectories(host)
- .compose(dataDirs -> findKeyspaceDirectory(dataDirs, request.getKeyspace()))
- .compose(keyspaceDirectory -> findTableDirectory(keyspaceDirectory, request.getTableName()))
- .compose(tableDirectory -> findComponent(tableDirectory, request.getSnapshotName(),
- request.getComponentName()));
+ .compose(dataDirs -> findKeyspaceDirectory(dataDirs, request.keyspace()))
+ .compose(keyspaceDirectory -> findTableDirectory(keyspaceDirectory, request.tableName()))
+ .compose(tableDirectory -> findComponent(tableDirectory, request.snapshotName(),
+ request.componentName()));
}
/**
@@ -116,9 +116,9 @@ public class SnapshotPathBuilder
public Future<String> build(String host, ListSnapshotFilesRequest request)
{
return getDataDirectories(host)
- .compose(dataDirs -> findKeyspaceDirectory(dataDirs, request.getKeyspace()))
- .compose(keyspaceDirectory -> findTableDirectory(keyspaceDirectory, request.getTableName()))
- .compose(tableDirectory -> findSnapshotDirectory(tableDirectory, request.getSnapshotName()));
+ .compose(dataDirs -> findKeyspaceDirectory(dataDirs, request.keyspace()))
+ .compose(keyspaceDirectory -> findTableDirectory(keyspaceDirectory, request.tableName()))
+ .compose(tableDirectory -> findSnapshotDirectory(tableDirectory, request.snapshotName()));
}
/**
@@ -294,7 +294,7 @@ public class SnapshotPathBuilder
protected void validate(StreamSSTableComponentRequest request)
{
// Only allow .db and TOC.txt components here
- validator.validateRestrictedComponentName(request.getComponentName());
+ validator.validateRestrictedComponentName(request.componentName());
}
/**
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java b/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
index 53de99f..7e6e505 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
@@ -112,7 +112,7 @@ public class FileStreamer
if (!isRateLimited() || acquire(response, filename, fileLength, range, startTime, promise))
{
// Stream data if rate limiting is disabled or if we acquire
- LOGGER.info("Streaming range {} for file {} to client {}. Instance: {}", range, filename,
+ LOGGER.debug("Streaming range {} for file {} to client {}. Instance: {}", range, filename,
response.remoteAddress(), response.host());
response.sendFile(filename, fileLength, range)
.onSuccess(v ->
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
new file mode 100644
index 0000000..9b37781
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.routes;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.Session;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.predicate.ResponsePredicate;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.Configuration;
+import org.apache.cassandra.sidecar.MainModule;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.InstancesConfig;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.CQLSession;
+import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the {@link SchemaHandler}
+ */
+@ExtendWith(VertxExtension.class)
+class SchemaHandlerTest
+{
+ static final Logger LOGGER = LoggerFactory.getLogger(SchemaHandlerTest.class);
+ Vertx vertx;
+ Configuration config;
+ HttpServer server;
+ @TempDir
+ File dataDir0;
+ String testKeyspaceSchema;
+
+ @SuppressWarnings("DataFlowIssue")
+ @BeforeEach
+ void before() throws InterruptedException, IOException
+ {
+ ClassLoader cl = getClass().getClassLoader();
+ testKeyspaceSchema = IOUtils.toString(cl.getResourceAsStream("schema/test_keyspace_schema.cql"),
+ StandardCharsets.UTF_8);
+
+ Injector injector;
+ injector = Guice.createInjector(Modules.override(new MainModule())
+ .with(Modules.override(new TestModule())
+ .with(new SchemaHandlerTestModule())));
+ vertx = injector.getInstance(Vertx.class);
+ server = injector.getInstance(HttpServer.class);
+ config = injector.getInstance(Configuration.class);
+ VertxTestContext context = new VertxTestContext();
+ server.listen(config.getPort(), config.getHost(), context.succeedingThenComplete());
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ }
+
+ @AfterEach
+ void after() throws InterruptedException
+ {
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ server.close(res -> closeLatch.countDown());
+ vertx.close();
+ if (closeLatch.await(60, TimeUnit.SECONDS))
+ LOGGER.info("Close event received before timeout.");
+ else
+ LOGGER.error("Close event timed out.");
+ }
+
+ @Test
+ void testAllKeyspacesSchema(VertxTestContext context)
+ {
+ WebClient client = WebClient.create(vertx);
+ String testRoute = "/api/v1/schema/keyspaces";
+ client.get(config.getPort(), config.getHost(), testRoute)
+ .expect(ResponsePredicate.SC_OK)
+ .send(context.succeeding(response -> context.verify(() ->
+ {
+ assertThat(response.statusCode()).isEqualTo(OK.code());
+ JsonObject jsonObject = response.bodyAsJsonObject();
+ assertThat(jsonObject.getString("keyspace")).isNull();
+ assertThat(jsonObject.getString("schema"))
+ .isEqualTo("FULL SCHEMA");
+ context.completeNow();
+ })));
+ }
+
+ @Test
+ void testKeyspaceSchema(VertxTestContext context)
+ {
+ WebClient client = WebClient.create(vertx);
+ String testRoute = "/api/v1/schema/keyspaces/testKeyspace";
+ client.get(config.getPort(), config.getHost(), testRoute)
+ .expect(ResponsePredicate.SC_OK)
+ .send(context.succeeding(response -> context.verify(() ->
+ {
+ assertThat(response.statusCode()).isEqualTo(OK.code());
+ JsonObject jsonObject = response.bodyAsJsonObject();
+ assertThat(jsonObject.getString("keyspace")).isEqualTo("testKeyspace");
+ assertThat(jsonObject.getString("schema"))
+ .isEqualTo(testKeyspaceSchema);
+ context.completeNow();
+ })));
+ }
+
+ @Test
+ void testGetKeyspaceDoesNotExist(VertxTestContext context)
+ {
+ WebClient client = WebClient.create(vertx);
+ String testRoute = "/api/v1/schema/keyspaces/nonExistent";
+ client.get(config.getPort(), config.getHost(), testRoute)
+ .expect(ResponsePredicate.SC_NOT_FOUND)
+ .send(context.succeeding(response -> context.verify(() ->
+ {
+ assertThat(response.statusCode()).isEqualTo(NOT_FOUND.code());
+ context.completeNow();
+ })));
+ }
+
+ public class SchemaHandlerTestModule extends AbstractModule
+ {
+ @Provides
+ @Singleton
+ public InstancesConfig getInstanceConfig() throws IOException
+ {
+ final int instanceId = 100;
+ final String host = "127.0.0.1";
+ final InstanceMetadata instanceMetadata = mock(InstanceMetadata.class);
+ when(instanceMetadata.host()).thenReturn(host);
+ when(instanceMetadata.port()).thenReturn(9042);
+ when(instanceMetadata.dataDirs()).thenReturn(Collections.singletonList(dataDir0.getCanonicalPath()));
+ when(instanceMetadata.id()).thenReturn(instanceId);
+ when(instanceMetadata.delegate()).thenReturn(mock(CassandraAdapterDelegate.class));
+ CQLSession mockCqlSession = mock(CQLSession.class);
+ Session mockSession = mock(Session.class);
+ when(mockCqlSession.getLocalCql()).thenReturn(mockSession);
+ Cluster mockCluster = mock(Cluster.class);
+ Metadata mockMetadata = mock(Metadata.class);
+ KeyspaceMetadata mockKeyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(mockMetadata.exportSchemaAsString()).thenReturn("FULL SCHEMA");
+ when(mockMetadata.getKeyspace("testKeyspace")).thenReturn(mockKeyspaceMetadata);
+ when(mockKeyspaceMetadata.exportAsString()).thenReturn(testKeyspaceSchema);
+
+ when(mockCluster.getMetadata()).thenReturn(mockMetadata);
+ when(mockSession.getCluster()).thenReturn(mockCluster);
+ when(instanceMetadata.session()).thenReturn(mockCqlSession);
+
+ InstancesConfig mockInstancesConfig = mock(InstancesConfig.class);
+ when(mockInstancesConfig.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+ when(mockInstancesConfig.instanceFromId(instanceId)).thenReturn(instanceMetadata);
+ when(mockInstancesConfig.instanceFromHost(host)).thenReturn(instanceMetadata);
+
+ return mockInstancesConfig;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
index 2e788c6..61ba7ba 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
@@ -316,8 +316,8 @@ public class StreamSSTableComponentHandlerTest
{
assertThat(response.statusCode()).isEqualTo(OK.code());
assertThat(response.getHeader(HttpHeaderNames.CONTENT_LENGTH.toString()))
- .isEqualTo("4")
- .describedAs("Server should shrink the range to the file length");
+ .describedAs("Server should shrink the range to the file length")
+ .isEqualTo("4");
context.completeNow();
})));
}
diff --git a/src/test/resources/schema/test_keyspace_schema.cql b/src/test/resources/schema/test_keyspace_schema.cql
new file mode 100644
index 0000000..aabf610
--- /dev/null
+++ b/src/test/resources/schema/test_keyspace_schema.cql
@@ -0,0 +1,63 @@
+CREATE KEYSPACE testkeyspace WITH REPLICATION = { 'class' : 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': '1' }
+ AND DURABLE_WRITES = true;
+
+CREATE TYPE testkeyspace.cidr_range (
+ id uuid,
+ start inet,
+ end inet,
+ description text
+ );
+
+CREATE TABLE testkeyspace.testtable
+(
+ partition_key_1 uuid,
+ partition_key_2 text,
+ partition_key_3 frozen<set<bigint>>,
+ clustering_column_1 date,
+ clustering_column_2 inet,
+ column_1 blob,
+ column_2 duration,
+ column_3 map<text, bigint>,
+ PRIMARY KEY ((partition_key_1, partition_key_2, partition_key_3),
+ clustering_column_1,
+ clustering_column_2
+ )
+) WITH CLUSTERING ORDER BY (clustering_column_1 ASC, clustering_column_2 ASC)
+ AND read_repair = 'BLOCKING'
+ AND gc_grace_seconds = 864000
+ AND additional_write_policy = '99p'
+ AND bloom_filter_fp_chance = 0.01
+ AND caching = { 'keys' : 'ALL', 'rows_per_partition' : 'NONE' }
+ AND comment = ''
+ AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold' : 32, 'min_threshold' : 4 }
+ AND compression = { 'chunk_length_in_kb' : 16, 'class' : 'org.apache.cassandra.io.compress.LZ4Compressor' }
+ AND default_time_to_live = 0
+ AND speculative_retry = '99p'
+ AND min_index_interval = 128
+ AND max_index_interval = 2048
+ AND crc_check_chance = 1.0
+ AND cdc = false
+ AND memtable_flush_period_in_ms = 0;
+
+CREATE TABLE testkeyspace.testtable2
+(
+ partition_key_1 uuid,
+ partition_key_2 text,
+ column_1 testkeyspace.cidr_range,
+ PRIMARY KEY ((partition_key_1, partition_key_2)
+ )
+) WITH read_repair = 'BLOCKING'
+ AND gc_grace_seconds = 864000
+ AND additional_write_policy = '99p'
+ AND bloom_filter_fp_chance = 0.01
+ AND caching = { 'keys' : 'ALL', 'rows_per_partition' : 'NONE' }
+ AND comment = ''
+ AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold' : 32, 'min_threshold' : 4 }
+ AND compression = { 'chunk_length_in_kb' : 16, 'class' : 'org.apache.cassandra.io.compress.LZ4Compressor' }
+ AND default_time_to_live = 0
+ AND speculative_retry = '99p'
+ AND min_index_interval = 128
+ AND max_index_interval = 2048
+ AND crc_check_chance = 1.0
+ AND cdc = false
+ AND memtable_flush_period_in_ms = 0;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org