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 c7b170c CASSANDRASC-74: Stream sstable components API fails on secondary index files c7b170c is described below commit c7b170cf2e7764159a3d9cf4ca0abc6db1659e51 Author: Francisco Guerrero <fran...@apache.org> AuthorDate: Thu Sep 21 14:16:56 2023 -0700 CASSANDRASC-74: Stream sstable components API fails on secondary index files In this commit, we fix streaming secondary index SSTable component files. We add tests to validate that the index files can be streamed. We also add compatibility for older clients that don't have the fix. Patch by Francisco Guerrero; Reviewed by Dinesh Joshi, Yifan Cai for CASSANDRASC-74 --- CHANGES.txt | 1 + .../cassandra/sidecar/common/ApiEndpointsV1.java | 5 + .../sidecar/common/data/SSTableComponent.java | 19 ++- .../cassandra/sidecar/CassandraSidecarDaemon.java | 2 +- .../org/apache/cassandra/sidecar/MainModule.java | 5 + .../data/StreamSSTableComponentRequest.java | 37 ++++- .../routes/StreamSSTableComponentHandler.java | 58 +++---- .../sidecar/snapshots/SnapshotPathBuilder.java | 34 ++++- .../sidecar/utils/CassandraInputValidator.java | 2 +- ...reamSSTableComponentHandlerIntegrationTest.java | 170 +++++++++++++++++++++ .../data/StreamSSTableComponentRequestTest.java | 25 ++- .../routes/StreamSSTableComponentHandlerTest.java | 3 +- .../snapshots/AbstractSnapshotPathBuilderTest.java | 52 ++++++- 13 files changed, 359 insertions(+), 54 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4dff0fd..255f614 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 1.0.0 ----- + * Fix unable to stream secondary index files (CASSANDRASC-74) * Updates token-ranges endpoint to return additional instance metadata (CASSANDRASC-73) * Shade Jackson completely to prevent incompatibility issues (CASSANDRASC-75) * Adds endpoint to serve read/write replica-sets by token-ranges (CASSANDRASC-60) diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java index 77ad601..5fe2d5b 100644 --- a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java +++ b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java @@ -32,12 +32,15 @@ public final class ApiEndpointsV1 public static final String TABLE_PATH_PARAM = ":table"; public static final String SNAPSHOT_PATH_PARAM = ":snapshot"; public static final String COMPONENT_PATH_PARAM = ":component"; + public static final String INDEX_PATH_PARAM = ":index"; public static final String UPLOAD_ID_PATH_PARAM = ":uploadId"; public static final String PER_KEYSPACE = "/keyspaces/" + KEYSPACE_PATH_PARAM; public static final String PER_TABLE = "/tables/" + TABLE_PATH_PARAM; public static final String PER_SNAPSHOT = "/snapshots/" + SNAPSHOT_PATH_PARAM; public static final String PER_COMPONENT = "/components/" + COMPONENT_PATH_PARAM; + public static final String PER_SECONDARY_INDEX_COMPONENT = "/components/" + INDEX_PATH_PARAM + + "/" + COMPONENT_PATH_PARAM; public static final String PER_UPLOAD = "/uploads/" + UPLOAD_ID_PATH_PARAM; public static final String HEALTH_ROUTE = API_V1 + HEALTH; @@ -55,6 +58,8 @@ public final class ApiEndpointsV1 public static final String SNAPSHOTS_ROUTE = API_V1 + PER_KEYSPACE + PER_TABLE + PER_SNAPSHOT; // Replaces DEPRECATED_COMPONENTS_ROUTE public static final String COMPONENTS_ROUTE = SNAPSHOTS_ROUTE + PER_COMPONENT; + public static final String COMPONENTS_WITH_SECONDARY_INDEX_ROUTE_SUPPORT = SNAPSHOTS_ROUTE + + PER_SECONDARY_INDEX_COMPONENT; @Deprecated public static final String DEPRECATED_ALL_KEYSPACES_SCHEMA_ROUTE = API_V1 + "/schema/keyspaces"; 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 00acd0f..5f28565 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 @@ -20,6 +20,8 @@ package org.apache.cassandra.sidecar.common.data; import java.util.Objects; +import org.jetbrains.annotations.Nullable; + /** * Represents an SSTable component that includes a keyspace, table name and component name */ @@ -27,16 +29,22 @@ public class SSTableComponent { private final QualifiedTableName qualifiedTableName; private final String componentName; + @Nullable + private final String secondaryIndexName; /** * Constructor for the holder class * * @param qualifiedTableName the qualified table name in Cassandra + * @param secondaryIndexName the name of the secondary index for the SSTable component * @param componentName the name of the SSTable component */ - public SSTableComponent(QualifiedTableName qualifiedTableName, String componentName) + public SSTableComponent(QualifiedTableName qualifiedTableName, + @Nullable String secondaryIndexName, + String componentName) { this.qualifiedTableName = Objects.requireNonNull(qualifiedTableName, "qualifiedTableName must not be null"); + this.secondaryIndexName = secondaryIndexName; this.componentName = Objects.requireNonNull(componentName, "componentName must not be null"); } @@ -64,6 +72,15 @@ public class SSTableComponent return qualifiedTableName.tableName(); } + /** + * @return the secondary index name when the SSTable component is an index component, {@code null} otherwise + */ + @Nullable + public String secondaryIndexName() + { + return secondaryIndexName; + } + /** * @return the name of the SSTable component */ diff --git a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java index 883675b..9ebf3ac 100644 --- a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java +++ b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java @@ -111,7 +111,7 @@ public class CassandraSidecarDaemon try { // Some closing action is executed on the executorPool (which is closed when closing vertx). - // Reflecting the dependncy below. + // Reflecting the dependency below. CompositeFuture.all(closingFutures) .onComplete(v -> vertx.close()).toCompletionStage() diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/MainModule.java index f530898..924ac83 100644 --- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java +++ b/src/main/java/org/apache/cassandra/sidecar/MainModule.java @@ -181,6 +181,11 @@ public class MainModule extends AbstractModule .handler(streamSSTableComponentHandler) .handler(fileStreamHandler); + // Support for routes that want to stream SStable index components + router.get(ApiEndpointsV1.COMPONENTS_WITH_SECONDARY_INDEX_ROUTE_SUPPORT) + .handler(streamSSTableComponentHandler) + .handler(fileStreamHandler); + //noinspection deprecation router.get(ApiEndpointsV1.DEPRECATED_SNAPSHOTS_ROUTE) .handler(snapshotsHandler); diff --git a/src/main/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequest.java b/src/main/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequest.java index 9fa8bbd..637ca55 100644 --- a/src/main/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequest.java +++ b/src/main/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequest.java @@ -20,8 +20,10 @@ package org.apache.cassandra.sidecar.data; import java.util.Objects; +import io.vertx.ext.web.RoutingContext; import org.apache.cassandra.sidecar.common.data.QualifiedTableName; import org.apache.cassandra.sidecar.common.data.SSTableComponent; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; /** @@ -43,7 +45,23 @@ public class StreamSSTableComponentRequest extends SSTableComponent @VisibleForTesting public StreamSSTableComponentRequest(String keyspace, String tableName, String snapshotName, String componentName) { - this(new QualifiedTableName(keyspace, tableName, true), snapshotName, componentName); + this(new QualifiedTableName(keyspace, tableName, true), snapshotName, null, componentName); + } + + /** + * Constructor for the holder class + * + * @param keyspace the keyspace in Cassandra + * @param tableName the table name in Cassandra + * @param snapshotName the name of the snapshot + * @param secondaryIndexName the name of the secondary index for the SSTable component + * @param componentName the name of the SSTable component + */ + @VisibleForTesting + public StreamSSTableComponentRequest(String keyspace, String tableName, String snapshotName, + String secondaryIndexName, String componentName) + { + this(new QualifiedTableName(keyspace, tableName, true), snapshotName, secondaryIndexName, componentName); } /** @@ -51,16 +69,30 @@ public class StreamSSTableComponentRequest extends SSTableComponent * * @param qualifiedTableName the qualified table name in Cassandra * @param snapshotName the name of the snapshot + * @param secondaryIndexName the name of the secondary index for the SSTable component * @param componentName the name of the SSTable component */ public StreamSSTableComponentRequest(QualifiedTableName qualifiedTableName, String snapshotName, + @Nullable String secondaryIndexName, String componentName) { - super(qualifiedTableName, componentName); + super(qualifiedTableName, secondaryIndexName, componentName); this.snapshotName = Objects.requireNonNull(snapshotName, "snapshotName must not be null"); } + public static StreamSSTableComponentRequest from(QualifiedTableName qualifiedTableName, RoutingContext context) + { + String snapshotName = context.pathParam("snapshot"); + String secondaryIndexName = context.pathParam("index"); + String componentName = context.pathParam("component"); + + return new StreamSSTableComponentRequest(qualifiedTableName, + snapshotName, + secondaryIndexName, + componentName); + } + /** * @return the name of the snapshot */ @@ -78,6 +110,7 @@ public class StreamSSTableComponentRequest extends SSTableComponent "keyspace='" + keyspace() + '\'' + ", tableName='" + tableName() + '\'' + ", snapshot='" + snapshotName + '\'' + + ", secondaryIndexName='" + secondaryIndexName() + '\'' + ", componentName='" + componentName() + '\'' + '}'; } diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandler.java index 6d3abdf..93dae6e 100644 --- a/src/main/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandler.java +++ b/src/main/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandler.java @@ -24,7 +24,6 @@ import java.nio.file.NoSuchFileException; 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.http.HttpServerRequest; import io.vertx.core.net.SocketAddress; import io.vertx.ext.web.RoutingContext; @@ -44,7 +43,6 @@ import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpExceptio public class StreamSSTableComponentHandler extends AbstractHandler<StreamSSTableComponentRequest> { private final SnapshotPathBuilder snapshotPathBuilder; - private final CassandraInputValidator validator; @Inject public StreamSSTableComponentHandler(InstanceMetadataFetcher metadataFetcher, @@ -54,7 +52,6 @@ public class StreamSSTableComponentHandler extends AbstractHandler<StreamSSTable { super(metadataFetcher, executorPools, validator); this.snapshotPathBuilder = snapshotPathBuilder; - this.validator = validator; } @Override @@ -64,43 +61,32 @@ public class StreamSSTableComponentHandler extends AbstractHandler<StreamSSTable SocketAddress remoteAddress, StreamSSTableComponentRequest request) { - validate(request) - .compose(validParams -> - snapshotPathBuilder.build(host, validParams) - .onSuccess(path -> { - logger.debug("StreamSSTableComponentHandler handled {} for client {}. " - + "Instance: {}", path, remoteAddress, host); - context.put(FileStreamHandler.FILE_PATH_CONTEXT_KEY, path) - .next(); - })) - .onFailure(cause -> { - String errMsg = - "StreamSSTableComponentHandler failed for request: {} from: {}. Instance: {}"; - logger.error(errMsg, request, remoteAddress, host, cause); - if (cause instanceof NoSuchFileException) - { - context.fail(wrapHttpException(HttpResponseStatus.NOT_FOUND, cause.getMessage())); - } - else - { - context.fail(wrapHttpException(HttpResponseStatus.BAD_REQUEST, "Invalid request for " + request)); - } - }); + snapshotPathBuilder.build(host, request) + .onSuccess(path -> { + logger.debug("StreamSSTableComponentHandler handled {} for client {}. " + + "Instance: {}", path, remoteAddress, host); + context.put(FileStreamHandler.FILE_PATH_CONTEXT_KEY, path) + .next(); + }) + .onFailure(cause -> { + String errMsg = + "StreamSSTableComponentHandler failed for request: {} from: {}. Instance: {}"; + logger.error(errMsg, request, remoteAddress, host, cause); + if (cause instanceof NoSuchFileException) + { + context.fail(wrapHttpException(HttpResponseStatus.NOT_FOUND, cause.getMessage())); + } + else + { + context.fail(wrapHttpException(HttpResponseStatus.BAD_REQUEST, + "Invalid request for " + request)); + } + }); } @Override protected StreamSSTableComponentRequest extractParamsOrThrow(RoutingContext context) { - return new StreamSSTableComponentRequest(qualifiedTableName(context), - context.pathParam("snapshot"), - context.pathParam("component") - ); - } - - private Future<StreamSSTableComponentRequest> validate(StreamSSTableComponentRequest request) - { - validator.validateComponentName(request.componentName()); - validator.validateSnapshotName(request.snapshotName()); - return Future.succeededFuture(request); + return StreamSSTableComponentRequest.from(qualifiedTableName(context), context); } } 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 9f5d9d3..2077f0f 100644 --- a/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java +++ b/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java @@ -44,11 +44,13 @@ import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.file.FileProps; import org.apache.cassandra.sidecar.cluster.InstancesConfig; +import org.apache.cassandra.sidecar.common.utils.Preconditions; import org.apache.cassandra.sidecar.concurrent.ExecutorPools; import org.apache.cassandra.sidecar.data.SnapshotRequest; import org.apache.cassandra.sidecar.data.StreamSSTableComponentRequest; import org.apache.cassandra.sidecar.utils.BaseFileSystem; import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.jetbrains.annotations.Nullable; /** * This class builds the snapshot path on a given host validating that it exists @@ -94,7 +96,9 @@ public class SnapshotPathBuilder extends BaseFileSystem return dataDirectories(host) .compose(dataDirs -> findKeyspaceDirectory(dataDirs, request.keyspace())) .compose(keyspaceDirectory -> findTableDirectory(keyspaceDirectory, request.tableName())) - .compose(tableDirectory -> findComponent(tableDirectory, request.snapshotName(), + .compose(tableDirectory -> findComponent(tableDirectory, + request.snapshotName(), + request.secondaryIndexName(), request.componentName())); } @@ -283,6 +287,13 @@ public class SnapshotPathBuilder extends BaseFileSystem validator.validateTableName(request.tableName()); validator.validateSnapshotName(request.snapshotName()); // Only allow .db and TOC.txt components here + String secondaryIndexName = request.secondaryIndexName(); + if (secondaryIndexName != null) + { + Preconditions.checkArgument(!secondaryIndexName.isEmpty(), "secondaryIndexName cannot be empty"); + Preconditions.checkArgument(secondaryIndexName.charAt(0) == '.', "Invalid secondary index name"); + validator.validatePattern(secondaryIndexName.substring(1), "secondary index"); + } validator.validateRestrictedComponentName(request.componentName()); } @@ -375,16 +386,23 @@ public class SnapshotPathBuilder extends BaseFileSystem * Constructs the path to the component using the {@code baseDirectory}, {@code snapshotName}, and * {@code componentName} and returns if it is a valid path to the component, or a failure otherwise. * - * @param baseDirectory the base directory where we search the table directory - * @param snapshotName the name of the snapshot - * @param componentName the name of the component + * @param baseDirectory the base directory where we search the table directory + * @param snapshotName the name of the snapshot + * @param secondaryIndexName the name of the secondary index (if provided) + * @param componentName the name of the component * @return the path to the component if it's valid, a failure otherwise */ - protected Future<String> findComponent(String baseDirectory, String snapshotName, String componentName) + protected Future<String> findComponent(String baseDirectory, String snapshotName, + @Nullable String secondaryIndexName, String componentName) { - String componentFilename = StringUtils.removeEnd(baseDirectory, File.separator) + - File.separator + SNAPSHOTS_DIR_NAME + File.separator + snapshotName + - File.separator + componentName; + StringBuilder sb = new StringBuilder(StringUtils.removeEnd(baseDirectory, File.separator)) + .append(File.separator).append(SNAPSHOTS_DIR_NAME) + .append(File.separator).append(snapshotName); + if (secondaryIndexName != null) + { + sb.append(File.separator).append(secondaryIndexName); + } + String componentFilename = sb.append(File.separator).append(componentName).toString(); return isValidFilename(componentFilename) .recover(t -> { diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java b/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java index dc4bf8c..838388e 100644 --- a/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java +++ b/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java @@ -166,7 +166,7 @@ public class CassandraInputValidator * @param name a name for the exception * @throws HttpException when the {@code input} does not match the pattern */ - private void validatePattern(String input, String name) + public void validatePattern(String input, String name) { if (!input.matches(validationConfiguration.allowedPatternForDirectory())) throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java new file mode 100644 index 0000000..a53329e --- /dev/null +++ b/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java @@ -0,0 +1,170 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.extension.ExtendWith; + +import com.datastax.driver.core.Session; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.predicate.ResponsePredicate; +import io.vertx.ext.web.codec.BodyCodec; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.apache.cassandra.sidecar.IntegrationTestBase; +import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse; +import org.apache.cassandra.sidecar.common.data.QualifiedTableName; +import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext; +import org.apache.cassandra.testing.CassandraIntegrationTest; + +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(VertxExtension.class) +class StreamSSTableComponentHandlerIntegrationTest extends IntegrationTestBase +{ + @CassandraIntegrationTest + void testStreamIncludingIndexFiles(VertxTestContext context) throws InterruptedException + { + runTestScenario(context); + } + + private void runTestScenario(VertxTestContext context) throws InterruptedException + { + createTestKeyspace(); + QualifiedTableName table = createTestTableAndPopulate(sidecarTestContext); + + List<String> expectedFileList = Arrays.asList(".ryear/[a-z]{2}-1-big-Data.db", + ".ryear/[a-z]{2}-1-big-TOC.txt", + "[a-z]{2}-1-big-Data.db", + "[a-z]{2}-1-big-TOC.txt"); + + WebClient client = WebClient.create(vertx); + String testRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot", + table.keyspace(), table.tableName()); + + createSnapshot(client, testRoute) + .compose(route -> listSnapshot(client, route)) + .onComplete(context.succeeding(response -> { + + List<ListSnapshotFilesResponse.FileInfo> filesToStream = + response.snapshotFilesInfo() + .stream() + .filter(info -> info.fileName.endsWith("-Data.db") || info.fileName.endsWith("-TOC.txt")) + .sorted(Comparator.comparing(o -> o.fileName)) + .collect(Collectors.toList()); + + assertThat(filesToStream).hasSize(4); + for (int i = 0; i < filesToStream.size(); i++) + { + ListSnapshotFilesResponse.FileInfo fileInfo = filesToStream.get(i); + assertThat(fileInfo.fileName).matches(expectedFileList.get(i)); + } + + List<Future> futures = new ArrayList<>(); + // Stream all the files including index files + for (ListSnapshotFilesResponse.FileInfo fileInfo : filesToStream) + { + futures.add(streamSSTableComponent(client, fileInfo)); + } + + CompositeFuture.all(futures) + .onSuccess(s -> context.completeNow()) + .onFailure(context::failNow); + })); + + // wait until test completes + assertThat(context.awaitCompletion(30, TimeUnit.SECONDS)).isTrue(); + } + + Future<String> createSnapshot(WebClient client, String route) + { + Promise<String> promise = Promise.promise(); + client.put(server.actualPort(), "127.0.0.1", route) + .expect(ResponsePredicate.SC_OK) + .send() + .onSuccess(response -> { + assertThat(response.statusCode()).isEqualTo(OK.code()); + promise.complete(route); + }) + .onFailure(promise::fail); + return promise.future(); + } + + Future<ListSnapshotFilesResponse> listSnapshot(WebClient client, String route) + { + Promise<ListSnapshotFilesResponse> promise = Promise.promise(); + client.get(server.actualPort(), "127.0.0.1", route + "?includeSecondaryIndexFiles=true") + .expect(ResponsePredicate.SC_OK) + .send() + .onSuccess(response -> { + assertThat(response.statusCode()).isEqualTo(OK.code()); + promise.complete(response.bodyAsJson(ListSnapshotFilesResponse.class)); + }) + .onFailure(promise::fail); + return promise.future(); + } + + Future<HttpResponse<Buffer>> streamSSTableComponent(WebClient client, + ListSnapshotFilesResponse.FileInfo fileInfo) + { + String route = "/keyspaces/" + fileInfo.keySpaceName + + "/tables/" + fileInfo.tableName + + "/snapshots/" + fileInfo.snapshotName + + "/components/" + fileInfo.fileName; + + return client.get(server.actualPort(), "localhost", "/api/v1" + route) + .expect(ResponsePredicate.SC_OK) + .as(BodyCodec.buffer()) + .send(); + } + + QualifiedTableName createTestTableAndPopulate(CassandraSidecarTestContext cassandraTestContext) + { + QualifiedTableName tableName = createTestTable( + "CREATE TABLE %s ( \n" + + " race_year int, \n" + + " race_name text, \n" + + " cyclist_name text, \n" + + " rank int, \n" + + " PRIMARY KEY ((race_year, race_name), rank) \n" + + ");"); + Session session = maybeGetSession(); + + session.execute("CREATE INDEX ryear ON " + tableName + " (race_year);"); + session.execute("INSERT INTO " + tableName + " (race_year, race_name, rank, cyclist_name) " + + "VALUES (2015, 'Tour of Japan - Stage 4 - Minami > Shinshu', 1, 'Benjamin PRADES');"); + session.execute("INSERT INTO " + tableName + " (race_year, race_name, rank, cyclist_name) " + + "VALUES (2015, 'Tour of Japan - Stage 4 - Minami > Shinshu', 2, 'Adam PHELAN');"); + session.execute("INSERT INTO " + tableName + " (race_year, race_name, rank, cyclist_name) " + + "VALUES (2015, 'Tour of Japan - Stage 4 - Minami > Shinshu', 3, 'Thomas LEBAS');"); + return tableName; + } +} diff --git a/src/test/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequestTest.java b/src/test/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequestTest.java index 4222535..50da1e1 100644 --- a/src/test/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequestTest.java +++ b/src/test/java/org/apache/cassandra/sidecar/data/StreamSSTableComponentRequestTest.java @@ -20,6 +20,8 @@ package org.apache.cassandra.sidecar.data; import org.junit.jupiter.api.Test; +import org.apache.cassandra.sidecar.common.data.QualifiedTableName; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -28,7 +30,8 @@ class StreamSSTableComponentRequestTest @Test void failsWhenKeyspaceIsNull() { - assertThatThrownBy(() -> new StreamSSTableComponentRequest(null, "table", "snapshot", "component")) + String keyspace = null; + assertThatThrownBy(() -> new StreamSSTableComponentRequest(keyspace, "table", "snapshot", "component")) .isInstanceOf(NullPointerException.class) .hasMessageContaining("keyspace must not be null"); } @@ -67,7 +70,25 @@ class StreamSSTableComponentRequestTest assertThat(req.tableName()).isEqualTo("table"); assertThat(req.snapshotName()).isEqualTo("snapshot"); assertThat(req.componentName()).isEqualTo("data.db"); + assertThat(req.secondaryIndexName()).isNull(); + assertThat(req.toString()).isEqualTo("StreamSSTableComponentRequest{keyspace='ks', tableName='table', " + + "snapshot='snapshot', secondaryIndexName='null', " + + "componentName='data.db'}"); + } + + @Test + void testValidRequestWithIndexName() + { + StreamSSTableComponentRequest req = + new StreamSSTableComponentRequest(new QualifiedTableName("ks", "table"), "snapshot", ".index", "data.db"); + + assertThat(req.keyspace()).isEqualTo("ks"); + assertThat(req.tableName()).isEqualTo("table"); + assertThat(req.snapshotName()).isEqualTo("snapshot"); + assertThat(req.secondaryIndexName()).isEqualTo(".index"); + assertThat(req.componentName()).isEqualTo("data.db"); assertThat(req.toString()).isEqualTo("StreamSSTableComponentRequest{keyspace='ks', tableName='table', " + - "snapshot='snapshot', componentName='data.db'}"); + "snapshot='snapshot', secondaryIndexName='.index', " + + "componentName='data.db'}"); } } 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 b769280..0c41b40 100644 --- a/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java +++ b/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java @@ -246,7 +246,8 @@ public class StreamSSTableComponentHandlerTest } @ParameterizedTest - @ValueSource(strings = { "i_❤_u.db", "this-is-not-allowed.jar", "../../../etc/passwd.db" }) + @ValueSource(strings = { "i_❤_u.db", "this-is-not-allowed.jar", "../../../etc/passwd.db", + "../not-an-index-file-Data.db" }) void failsWhenComponentNameContainsInvalidCharacters(String invalidComponentName) { VertxTestContext context = new VertxTestContext(); diff --git a/src/testFixtures/java/org/apache/cassandra/sidecar/snapshots/AbstractSnapshotPathBuilderTest.java b/src/testFixtures/java/org/apache/cassandra/sidecar/snapshots/AbstractSnapshotPathBuilderTest.java index e2dbd9b..a6727fc 100644 --- a/src/testFixtures/java/org/apache/cassandra/sidecar/snapshots/AbstractSnapshotPathBuilderTest.java +++ b/src/testFixtures/java/org/apache/cassandra/sidecar/snapshots/AbstractSnapshotPathBuilderTest.java @@ -44,7 +44,9 @@ import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl; import org.apache.cassandra.sidecar.data.SnapshotRequest; import org.apache.cassandra.sidecar.data.StreamSSTableComponentRequest; import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.assertj.core.api.InstanceOfAssertFactories; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.from; @@ -150,8 +152,7 @@ public abstract class AbstractSnapshotPathBuilderTest @ParameterizedTest @ValueSource(strings = { "i_❤_u.db", "this-is-not-allowed.jar", "cql-is-not-allowed-here.cql", - "json-is-not-allowed-here.json", "crc32-is-not-allowed-here.crc32", - "../../../etc/passwd.db" }) + "json-is-not-allowed-here.json", "crc32-is-not-allowed-here.crc32" }) void failsWhenComponentNameContainsInvalidCharacters(String invalidComponentName) { assertThatThrownBy(() -> instance.build("localhost", @@ -166,6 +167,53 @@ public abstract class AbstractSnapshotPathBuilderTest .getPayload())); } + @ParameterizedTest + @ValueSource(strings = { "f...@o-data.db", ".s./../../etc/passwd.db", "../../../bad-Data.db" }) + void failsWhenIndexComponentNameContainsInvalidCharacters(String invalidComponentName) + { + assertThatThrownBy(() -> instance.build("localhost", + new StreamSSTableComponentRequest("ks", + "table", + "snapshot", + ".index", + invalidComponentName))) + .isInstanceOf(HttpException.class) + .hasMessageContaining("Bad Request") + .returns(HttpResponseStatus.BAD_REQUEST.code(), from(t -> ((HttpException) t).getStatusCode())) + .returns("Invalid component name: " + invalidComponentName, from(t -> ((HttpException) t) + .getPayload())); + } + + @ParameterizedTest + @ValueSource(strings = { "", "does_not_start_with_dot" }) + void failsWhenIndexNameIsInvalid(String invalidIndexName) + { + assertThatThrownBy(() -> instance.build("localhost", + new StreamSSTableComponentRequest("ks", + "table", + "snapshot", + invalidIndexName, + "component.db"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = { ".", "../bad-Data.db", ".f@o/bad-Data.db", ".bl@h/bad-TOC.txt" }) + void failsWhenIndexNameContainsInvalidCharacters(String invalidIndexName) + { + assertThatThrownBy(() -> instance.build("localhost", + new StreamSSTableComponentRequest("ks", + "table", + "snapshot", + invalidIndexName, + "component.db"))) + .isInstanceOf(HttpException.class) + .hasMessageContaining("Bad Request") + .returns(HttpResponseStatus.BAD_REQUEST.code(), from(t -> ((HttpException) t).getStatusCode())) + .extracting(from(t -> ((HttpException) t).getPayload()), as(InstanceOfAssertFactories.STRING)) + .contains("Invalid characters in secondary index: "); + } + @Test void failsWhenDataDirsAreEmpty() { --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org