You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by fr...@apache.org on 2025/06/27 13:44:53 UTC
(cassandra-sidecar) branch trunk updated: CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)
This is an automated email from the ASF dual-hosted git repository.
frankgh 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 64f7dcd2 CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)
64f7dcd2 is described below
commit 64f7dcd26f4aaa4a38c475269aa49503327d39e4
Author: N V Harikrishna <n....@gmail.com>
AuthorDate: Fri Jun 27 19:14:48 2025 +0530
CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)
Patch by Venkata Harikrishna Nukala; reviewed by Yifan Cai, Francisco Guerrero for CASSSIDECAR-223
---
CHANGES.txt | 1 +
.../cassandra/sidecar/common/ApiEndpointsV1.java | 16 +-
.../acl/authorization/BasicPermissions.java | 1 +
.../livemigration/LiveMigrationDirType.java | 57 +++
.../LiveMigrationFileStreamHandler.java | 256 ++++++++++++
.../LiveMigrationInstanceMetadataUtil.java | 58 ++-
.../LiveMigrationPlaceholderUtil.java | 42 +-
.../sidecar/modules/LiveMigrationModule.java | 16 +
.../modules/multibindings/VertxRouteMapKeys.java | 5 +
.../LiveMigrationFileStreamHandlerTest.java | 439 +++++++++++++++++++++
.../LiveMigrationListInstanceFilesHandlerTest.java | 8 +-
.../CassandraInstanceFilesImplTest.java | 12 +-
.../LiveMigrationInstanceMetadataUtilTest.java | 64 ++-
.../LiveMigrationPlaceholderUtilTest.java | 95 +++++
14 files changed, 1039 insertions(+), 31 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 3b6984e1..b342cfe3 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
0.2.0
-----
+ * Add API that allows downloading of files listed by Live Migration (CASSSIDECAR-223)
* Fixing settings endpoint should return Service Unavailable when Cassandra instance is down (CASSSIDECAR-264)
* Add start/stop binary transport&gossip endpoints (CASSSIDECAR-253)
* Support a flag & implementation for only forward DNS resolution (CASSSIDECAR-263)
diff --git a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index cd47c812..9da97827 100644
--- a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++ b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -148,14 +148,14 @@ public final class ApiEndpointsV1
public static final String LIVE_MIGRATION_FILES_API = LIVE_MIGRATION_API_PREFIX + "/files";
- public static final String LIVE_MIGRATION_CDC_RAW_DIR_PATH = LIVE_MIGRATION_FILES_API + "/cdc_raw";
- public static final String LIVE_MIGRATION_COMMITLOG_DIR_PATH = LIVE_MIGRATION_FILES_API + "/commitlog";
- public static final String LIVE_MIGRATION_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API + "/data";
- public static final String LIVE_MIGRATION_HINTS_DIR_PATH = LIVE_MIGRATION_FILES_API + "/hints";
- public static final String LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API
- + "/local_system_data";
- public static final String LIVE_MIGRATION_SAVED_CACHES_DIR_PATH = LIVE_MIGRATION_FILES_API + "/saved_caches";
-
+ public static final String DIR_TYPE_PARAM = "dirType";
+ public static final String DIR_INDEX_PARAM = "dirIndex";
+ // API endpoint allows files transfer of specific directories handled by Cassandra during live migration operation.
+ // dirType path parameter : The type of directory (data, commitlog, etc.)
+ // dirIndex path parameter : The index of the directory
+ // The remaining path ('/*') : Represents the relative path within the specified directory
+ public static final String LIVE_MIGRATION_FILE_TRANSFER_API = LIVE_MIGRATION_FILES_API + "/:" + DIR_TYPE_PARAM
+ + "/:" + DIR_INDEX_PARAM + "/*";
private ApiEndpointsV1()
{
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
index a9ec98bf..0a809b1b 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
@@ -83,4 +83,5 @@ public class BasicPermissions
// Live Migration permissions
public static final Permission LIST_FILES = new DomainAwarePermission("LIVE_MIGRATION:LIST_FILES", CLUSTER_SCOPE);
+ public static final Permission STREAM_FILES = new DomainAwarePermission("LIVE_MIGRATION:STREAM", CLUSTER_SCOPE);
}
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java
new file mode 100644
index 00000000..18f724c2
--- /dev/null
+++ b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java
@@ -0,0 +1,57 @@
+/*
+ * 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.handlers.livemigration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enum for holding different type of directories handled by Live Migration.
+ */
+public enum LiveMigrationDirType
+{
+ CDC_RAW_DIR("cdc_raw"),
+ COMMIT_LOG_DIR("commitlog"),
+ DATA_FIlE_DIR("data"),
+ HINTS_DIR("hints"),
+ LOCAL_SYSTEM_DATA_FILE_DIR("local_system_data"),
+ SAVED_CACHES_DIR("saved_caches");
+
+ private static final Map<String, LiveMigrationDirType> DIR_TYPE_MAP = new HashMap<>();
+
+ static
+ {
+ for (LiveMigrationDirType type : values())
+ {
+ DIR_TYPE_MAP.put(type.dirType, type);
+ }
+ }
+
+ public final String dirType;
+
+ LiveMigrationDirType(String dirType)
+ {
+ this.dirType = dirType;
+ }
+
+ public static LiveMigrationDirType find(String dirType)
+ {
+ return DIR_TYPE_MAP.get(dirType);
+ }
+}
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java
new file mode 100644
index 00000000..902223fd
--- /dev/null
+++ b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java
@@ -0,0 +1,256 @@
+/*
+ * 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.handlers.livemigration;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.handlers.AbstractHandler;
+import org.apache.cassandra.sidecar.handlers.AccessProtected;
+import org.apache.cassandra.sidecar.handlers.FileStreamHandler;
+import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil;
+import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM;
+import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder;
+
+/**
+ * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler
+ * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow
+ * using "/.." in the path to access files. This handler does not serve files which are excluded in
+ * Live Migration configuration.
+ */
+public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected
+{
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class);
+
+ private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>();
+ private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>();
+ private final LiveMigrationConfiguration liveMigrationConfiguration;
+
+ @Inject
+ public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher,
+ ExecutorPools executorPools,
+ CassandraInputValidator validator,
+ SidecarConfiguration sidecarConfiguration)
+ {
+ super(metadataFetcher, executorPools, validator);
+ this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration();
+ }
+
+ @Override
+ protected Void extractParamsOrThrow(RoutingContext context)
+ {
+ String dirType = context.pathParam(DIR_TYPE_PARAM);
+ if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType))
+ {
+ throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType);
+ }
+
+ String dirIndex = context.pathParam(DIR_INDEX_PARAM);
+ int index;
+ try
+ {
+ index = Integer.parseInt(dirIndex);
+ }
+ catch (NumberFormatException formatException)
+ {
+ throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex);
+ }
+ if (index < 0)
+ {
+ throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex);
+ }
+
+ // Path params are not used further, hence returning null.
+ return null;
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext rc,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ @Nullable Void request)
+ {
+ String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8);
+
+ if (reqPath.contains("/../") || reqPath.endsWith("/.."))
+ {
+ LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath);
+ rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end();
+ return;
+ }
+
+ InstanceMetadata instanceMeta = metadataFetcher.instance(host);
+ String normalizedPath = rc.normalizedPath();
+ String localFile;
+
+ try
+ {
+ localFile = LiveMigrationInstanceMetadataUtil.localPath(normalizedPath, instanceMeta);
+ }
+ catch (IllegalArgumentException e)
+ {
+ LOGGER.warn("Invalid path", e);
+ rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+ return;
+ }
+
+ Path path = Paths.get(localFile);
+
+ executorPools.service()
+ .executeBlocking(() -> isInvalidPath(rc, path, reqPath, instanceMeta))
+ .onSuccess(invalid -> {
+ if (!invalid)
+ {
+ rc.put(FileStreamHandler.FILE_PATH_CONTEXT_KEY, localFile);
+ rc.next();
+ }
+ });
+ }
+
+ private boolean isInvalidPath(RoutingContext rc, Path path, String reqPath, InstanceMetadata instanceMeta)
+ {
+ if (!Files.exists(path))
+ {
+ LOGGER.info("Requested file is not found. file={}", path);
+ rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+ return true;
+ }
+ if (Files.isDirectory(path))
+ {
+ LOGGER.info("Cannot transfer directory. path={}.", reqPath);
+ rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end();
+ return true;
+ }
+ if (isExcluded(path, instanceMeta))
+ {
+ LOGGER.debug("Requested path or one of its parent directories is excluded from Live Migration. " +
+ "path={}", reqPath);
+ rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isExcluded(Path localFile, InstanceMetadata instanceMetadata)
+ {
+ return isFileExcluded(localFile, instanceMetadata) || isDirExcluded(localFile.getParent(), instanceMetadata);
+ }
+
+ private boolean isFileExcluded(Path localFile, InstanceMetadata instanceMetadata)
+ {
+ List<PathMatcher> fileExclusionMatchers = getPathMatchers(fileExclusionsByInstanceId,
+ instanceMetadata,
+ LiveMigrationConfiguration::filesToExclude);
+
+ return isMatch(localFile, fileExclusionMatchers);
+ }
+
+ private boolean isDirExcluded(Path dir, InstanceMetadata instanceMetadata)
+ {
+ List<PathMatcher> dirExclusionMatchers = getPathMatchers(dirExclusionsByInstanceId,
+ instanceMetadata,
+ LiveMigrationConfiguration::directoriesToExclude);
+
+ // Recursively check all parent directories to see if they are excluded or not.
+ while (dir != null)
+ {
+ if (isMatch(dir, dirExclusionMatchers))
+ {
+ return true;
+ }
+ dir = dir.getParent();
+ }
+
+ return false;
+ }
+
+ private List<PathMatcher> getPathMatchers(Map<Integer, List<PathMatcher>> map,
+ InstanceMetadata instanceMetadata,
+ Function<LiveMigrationConfiguration, Set<String>> exclusionProvider)
+ {
+ return map.computeIfAbsent(instanceMetadata.id(), id -> {
+ Set<String> exclusions = exclusionProvider.apply(liveMigrationConfiguration);
+ List<PathMatcher> matchers = new ArrayList<>(exclusions.size());
+ for (String placeholderPattern : exclusions)
+ {
+ Set<String> filePatterns = replacePlaceholder(placeholderPattern, instanceMetadata);
+ filePatterns.forEach(filePattern -> matchers.add(FileSystems.getDefault().getPathMatcher(filePattern)));
+ }
+ return matchers;
+ });
+ }
+
+ private boolean isMatch(Path localFile, List<PathMatcher> pathMatchers)
+ {
+ if (null == pathMatchers || pathMatchers.isEmpty())
+ {
+ return false;
+ }
+
+ for (PathMatcher pathMatcher : pathMatchers)
+ {
+ if (pathMatcher.matches(localFile))
+ {
+ LOGGER.debug("Requested file is excluded from Live Migration. file={}", localFile);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return Set.of(BasicPermissions.STREAM_FILES.toAuthorization());
+ }
+}
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
index ae2d814f..826ed1fe 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
@@ -31,14 +31,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
import org.jetbrains.annotations.NotNull;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.CDC_RAW_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.COMMIT_LOG_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.DATA_FIlE_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.HINTS_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.LOCAL_SYSTEM_DATA_FILE_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.SAVED_CACHES_DIR;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.DATA_FILE_DIR_PLACEHOLDER;
@@ -54,6 +55,13 @@ import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholde
public class LiveMigrationInstanceMetadataUtil
{
+ public static final String LIVE_MIGRATION_CDC_RAW_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + CDC_RAW_DIR.dirType;
+ public static final String LIVE_MIGRATION_COMMITLOG_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + COMMIT_LOG_DIR.dirType;
+ public static final String LIVE_MIGRATION_DATA_FILE_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + DATA_FIlE_DIR.dirType;
+ public static final String LIVE_MIGRATION_HINTS_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + HINTS_DIR.dirType;
+ public static final String LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API
+ + "/" + LOCAL_SYSTEM_DATA_FILE_DIR.dirType;
+ public static final String LIVE_MIGRATION_SAVED_CACHES_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + SAVED_CACHES_DIR.dirType;
private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationInstanceMetadataUtil.class);
/**
@@ -144,6 +152,44 @@ public class LiveMigrationInstanceMetadataUtil
return Collections.unmodifiableMap(placeholderMap);
}
+ /**
+ * Returns a map of placeholder and its directories based on given {@link InstanceMetadata}.
+ */
+ public static Map<String, Set<String>> placeholderDirsMap(InstanceMetadata instanceMetadata)
+ {
+ Map<String, Set<String>> placeholderDirsMap = new HashMap<>();
+
+ placeholderDirsMap.put(HINTS_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.hintsDir()));
+ placeholderDirsMap.put(COMMITLOG_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.commitlogDir()));
+
+ if (instanceMetadata.savedCachesDir() != null)
+ {
+ placeholderDirsMap.put(SAVED_CACHES_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.savedCachesDir()));
+ }
+
+ if (instanceMetadata.cdcDir() != null)
+ {
+ placeholderDirsMap.put(CDC_RAW_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.cdcDir()));
+ }
+
+ if (instanceMetadata.localSystemDataFileDir() != null)
+ {
+ placeholderDirsMap.put(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER,
+ Collections.singleton(instanceMetadata.localSystemDataFileDir()));
+ }
+
+ placeholderDirsMap.put(DATA_FILE_DIR_PLACEHOLDER, Set.copyOf(instanceMetadata.dataDirs()));
+
+ List<String> dataDirs = instanceMetadata.dataDirs();
+ for (int i = 0; i < dataDirs.size(); i++)
+ {
+ placeholderDirsMap.put(DATA_FILE_DIR_PLACEHOLDER + "_" + i, Collections.singleton(dataDirs.get(i)));
+ }
+
+ return placeholderDirsMap;
+ }
+
+
/**
* Converts given live migration file download URL to local path.
*
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
index a421f7d4..84bdc747 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
@@ -18,10 +18,14 @@
package org.apache.cassandra.sidecar.livemigration;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
import org.jetbrains.annotations.Nullable;
/**
@@ -61,12 +65,48 @@ public class LiveMigrationPlaceholderUtil
}
if (placeholders.contains(placeholder))
{
- return input.replace("${" + placeholder + "}", value);
+ return replacePlaceholder(input, placeholder, value);
}
return null;
}
+ /**
+ * Replaces placeholder present in given {@code input} with directory based on instance metadata.
+ *
+ * @param input Input string to replace placeholder
+ * @param instanceMetadata Metadata of instance to pick the directory for placeholder in input
+ * @return input after replacing placeholder with corresponding directories. If input does not
+ * have a placeholder, then input returned as Set.
+ */
+ public static Set<String> replacePlaceholder(String input, InstanceMetadata instanceMetadata)
+ {
+ if (!hasAnyPlaceholder(input))
+ {
+ return Collections.singleton(input);
+ }
+ String placeholder = getPlaceholder(input);
+ Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+ if (!placeholderDirsMap.containsKey(placeholder))
+ {
+ throw new IllegalArgumentException("Unknown placeholder specified in " + input);
+ }
+
+ Set<String> dirs = placeholderDirsMap.get(placeholder);
+ Set<String> output = new HashSet<>(dirs.size());
+ for (String dir : dirs)
+ {
+ output.add(replacePlaceholder(input, placeholder, dir));
+ }
+ return output;
+ }
+
+ private static String replacePlaceholder(String input, String placeholder, String value)
+ {
+ return input.replace("${" + placeholder + "}", value);
+ }
+
/**
* Checks whether given input has any placeholder init.
* Placeholders can be specified using {@link #PLACEHOLDER_PATTERN} pattern.
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java b/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
index 3e4f86f3..b2eeef8e 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
@@ -20,7 +20,9 @@ package org.apache.cassandra.sidecar.modules;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.ProvidesIntoMap;
+import org.apache.cassandra.sidecar.handlers.FileStreamHandler;
import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationApiEnableDisableHandler;
+import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationFileStreamHandler;
import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationListInstanceFilesHandler;
import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationMap;
import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationMapSidecarConfigImpl;
@@ -42,6 +44,20 @@ public class LiveMigrationModule extends AbstractModule
}
+ @ProvidesIntoMap
+ @KeyClassMapKey(VertxRouteMapKeys.LiveMigrationFileStreamHandlerRouteKey.class)
+ VertxRoute downloadFileRoute(RouteBuilder.Factory factory,
+ LiveMigrationApiEnableDisableHandler liveMigrationApiEnableDisableHandler,
+ LiveMigrationFileStreamHandler liveMigrationFileStreamHandler,
+ FileStreamHandler fileStreamHandler)
+ {
+ return factory.builderForRoute()
+ .handler(liveMigrationApiEnableDisableHandler::isSource)
+ .handler(liveMigrationFileStreamHandler)
+ .handler(fileStreamHandler)
+ .build();
+ }
+
@ProvidesIntoMap
@KeyClassMapKey(VertxRouteMapKeys.LiveMigrationListInstanceFilesRouteKey.class)
VertxRoute listInstanceFiles(RouteBuilder.Factory factory,
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
index 1706cc92..429ffb0c 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
@@ -192,6 +192,11 @@ public interface VertxRouteMapKeys
HttpMethod HTTP_METHOD = HttpMethod.GET;
String ROUTE_URI = ApiEndpointsV1.SNAPSHOTS_ROUTE;
}
+ interface LiveMigrationFileStreamHandlerRouteKey extends RouteClassKey
+ {
+ HttpMethod HTTP_METHOD = HttpMethod.GET;
+ String ROUTE_URI = ApiEndpointsV1.LIVE_MIGRATION_FILE_TRANSFER_API;
+ }
interface LiveMigrationListInstanceFilesRouteKey extends RouteClassKey
{
HttpMethod HTTP_METHOD = HttpMethod.GET;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java
new file mode 100644
index 00000000..f5c363d7
--- /dev/null
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java
@@ -0,0 +1,439 @@
+/*
+ * 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.handlers.livemigration;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+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.codahale.metrics.MetricRegistry;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.util.Modules;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.codec.BodyCodec;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.HelperTestModules;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadataImpl;
+import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(VertxExtension.class)
+class LiveMigrationFileStreamHandlerTest
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandlerTest.class);
+
+ private static final int FIRST_ID = 1000110;
+ private static final String FIRST_INSTANCE_IP = "127.0.0.1";
+ private static final int SECOND_ID = 1000111;
+ private static final String SECOND_INSTANCE_IP = "127.0.0.2";
+ private static final int THIRD_ID = 1000112;
+ private static final String THIRD_INSTANCE_IP = "127.0.0.3";
+ private static final String DUMMY_CONTENT = "data";
+
+ private static final MetricRegistryFactory REGISTRY_FACTORY =
+ new MetricRegistryFactory("cassandra_sidecar_" + UUID.randomUUID(), Collections.emptyList(), Collections.emptyList());
+ private final Vertx vertx = Vertx.vertx();
+ @TempDir
+ Path tempDir;
+ Server server;
+ List<String> firstInstanceDataDirs;
+ List<String> secondInstanceDataDirs;
+ List<String> thirdInstanceDataDirs;
+ private Injector injector;
+
+ @BeforeEach
+ public void setup() throws InterruptedException
+ {
+ InstanceMetadata firstInstanceMeta = getInstanceMetadata(FIRST_INSTANCE_IP, FIRST_ID);
+ firstInstanceDataDirs = firstInstanceMeta.dataDirs();
+ InstanceMetadata secondInstanceMeta = getInstanceMetadata(SECOND_INSTANCE_IP, SECOND_ID);
+ secondInstanceDataDirs = secondInstanceMeta.dataDirs();
+ InstanceMetadata thirdInstanceMeta = getInstanceMetadata(THIRD_INSTANCE_IP, THIRD_ID);
+ thirdInstanceDataDirs = thirdInstanceMeta.dataDirs();
+ FileStreamHandlerTestModule handlerTestModule = new FileStreamHandlerTestModule(
+ Arrays.asList(firstInstanceMeta, secondInstanceMeta, thirdInstanceMeta));
+ injector = Guice.createInjector(Modules.override(SidecarModules.all())
+ .with(Modules.override(new TestModule())
+ .with(handlerTestModule)));
+
+ server = injector.getInstance(Server.class);
+ VertxTestContext context = new VertxTestContext();
+ server.start()
+ .onSuccess(s -> context.completeNow())
+ .onFailure(context::failNow);
+ context.awaitCompletion(15, TimeUnit.SECONDS);
+ }
+
+ private InstanceMetadata getInstanceMetadata(String instanceIp,
+ int instanceId)
+ {
+ String root = tempDir.resolve(String.valueOf(instanceId)).toString();
+ List<String> dataDirs = Arrays.asList(root + "/d1/data", root + "/d2/data");
+ MetricRegistry instanceSpecificRegistry = REGISTRY_FACTORY.getOrCreate(instanceId);
+
+ return InstanceMetadataImpl.builder()
+ .id(instanceId)
+ .host(instanceIp)
+ .port(9042)
+ .dataDirs(dataDirs)
+ .hintsDir(root + "/hints")
+ .commitlogDir(root + "/commitlog")
+ .savedCachesDir(root + "/saved_caches")
+ .stagingDir(root + "/staging")
+ .metricRegistry(instanceSpecificRegistry)
+ .build();
+ }
+
+ @AfterEach
+ public void tearDown() throws InterruptedException
+ {
+ CountDownLatch closeLatch = new CountDownLatch(1);
+ server.close().onSuccess(res -> closeLatch.countDown());
+ if (closeLatch.await(60, TimeUnit.SECONDS))
+ LOGGER.info("Close event received before timeout.");
+ else
+ LOGGER.error("Close event timed out.");
+ }
+
+ @Test
+ public void testRouteSucceeds(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ shouldSucceed(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP);
+ }
+
+ @Test
+ public void testRouteUnConfiguredDirFile(final VertxTestContext context)
+ {
+ // CDC dir not configured while constructing dummy InstanceMetadata in getInstanceMetadata()
+ // Yet requesting a dummy file to see if the endpoint returns proper error
+
+ final String testRoute = LIVE_MIGRATION_CDC_RAW_DIR_PATH + "/0/Commitlog-7-1.db";
+ shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteDoubleDotAtTheEndAfterDirIndex(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/data/0/..",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRouteDoubleDotAtTheEnd(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234/ks-tb-1234-Data.db/..",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRouteDoubleDotAfterDirIndex(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/../ks/tb-1234/ks-tb-1234-Data.db",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRequestUsingEncodedDots(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // Using escape character of '.' i.e. %2E
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234/%2E%2E/%2E%2E/secrets",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+
+ @Test
+ public void testRequestInvalidPathUsingEncodedDots(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // Vertx is stopping routes having three "%2E%2E" in the path and returning 404
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/%2E%2E/%2E%2E/%2E%2E/secrets",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+
+ @Test
+ public void testRequestDirectory(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // Requesting directory, it should not succeed.
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234",
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRouteDirtyRouteThatDoesNotMatchPath(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/../../.." + filePath,
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteWithoutDirIndex(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // This route doesn't have data file dir index init. It should fail.
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + filePath,
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRouteHavingNegativeDirIndex(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // This route has negative dataHomeDir which is invalid. It should fail.
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/-1" + filePath,
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+ }
+
+ @Test
+ public void testRouteHavingDirIndexThatDoesNotExist(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ // This route has 100 as dataHomeDir index which is greater than instance data home dir count. It should fail.
+ shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/100" + filePath,
+ FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteFailsForDestinationInstance(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, THIRD_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteFailsForNonLiveMigratingInstance(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+ createFile(DUMMY_CONTENT, secondInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ // In this test second instance is neither source nor destination
+ shouldThrowError(context, testRoute, "127.0.0.4", "127.0.0.5", SECOND_INSTANCE_IP, 404);
+ }
+
+
+ @Test
+ public void testRouteFailsExcludedDataDirFile(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/test.txt";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ mockFileExclusion(Collections.singleton("glob:${DATA_FILE_DIR}" + filePath));
+
+ shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteFailsExcludedFileSecondDataDirWildcardExclusion(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/test.txt";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(1) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/1" + filePath;
+ mockFileExclusion(Collections.singleton("glob:${DATA_FILE_DIR_1}/ks/**"));
+
+ shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+
+ @Test
+ public void testRouteFailsWhenParentDirExcluded(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/test.txt";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ mockDirExclusion(Collections.singleton("glob:${DATA_FILE_DIR}/ks"));
+
+ shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+ }
+
+ @Test
+ public void testRouteSucceedsWhenOtherDirExcluded(final VertxTestContext context) throws IOException
+ {
+ String filePath = "/ks/tb-1234/test.txt";
+ createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+ final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+ mockDirExclusion(Collections.singleton("glob:${DATA_FILE_DIR}/ks2"));
+
+ shouldSucceed(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP);
+ }
+
+
+ @SuppressWarnings("SameParameterValue")
+ void shouldSucceed(final VertxTestContext context,
+ final String testRoute,
+ final String source,
+ final String destination,
+ String requestHost)
+ {
+ mockLiveMigrationMap(source, destination);
+
+ final WebClient client = WebClient.create(vertx);
+ client.get(server.actualPort(), requestHost, testRoute)
+ .as(BodyCodec.buffer())
+ .send(context.succeeding(resp -> context.verify(() -> {
+ assertThat(resp.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+ assertThat(resp.bodyAsString()).isEqualTo(DUMMY_CONTENT);
+ client.close();
+ context.completeNow();
+ })));
+ }
+
+ void shouldThrowError(final VertxTestContext context,
+ final String testRoute,
+ final String source,
+ final String destination,
+ String requestHost,
+ final int expectedStatusCode)
+ {
+ mockLiveMigrationMap(source, destination);
+
+ final WebClient client = WebClient.create(vertx);
+ final String url = String.format("http://%s:%d%s", requestHost, server.actualPort(), testRoute);
+ client.getAbs(url)
+ .as(BodyCodec.buffer())
+ .send(context.succeeding(resp -> context.verify(() -> {
+ assertThat(resp.statusCode()).isEqualTo(expectedStatusCode);
+ client.close();
+ context.completeNow();
+ })));
+ }
+
+ void mockLiveMigrationMap(final String source, final String destination)
+ {
+ LiveMigrationConfiguration liveMigrationConfig = injector.getInstance(SidecarConfiguration.class)
+ .liveMigrationConfiguration();
+ when(liveMigrationConfig.migrationMap()).thenReturn(Map.of(source, destination));
+ }
+
+ void mockFileExclusion(Set<String> fileExclusions)
+ {
+ LiveMigrationConfiguration configuration = injector.getInstance(SidecarConfiguration.class)
+ .liveMigrationConfiguration();
+ when(configuration.filesToExclude()).thenReturn(fileExclusions);
+ }
+
+ void mockDirExclusion(Set<String> dirExclusions)
+ {
+ LiveMigrationConfiguration configuration = injector.getInstance(SidecarConfiguration.class)
+ .liveMigrationConfiguration();
+ when(configuration.directoriesToExclude()).thenReturn(dirExclusions);
+ }
+
+
+ private static class FileStreamHandlerTestModule extends AbstractModule
+ {
+
+ private final List<InstanceMetadata> instanceMetaList;
+
+ public FileStreamHandlerTestModule(List<InstanceMetadata> instanceMetaList)
+ {
+ this.instanceMetaList = instanceMetaList;
+ }
+
+ @Override
+ protected void configure()
+ {
+
+ LiveMigrationConfiguration mockLiveMigrationConfiguration = mock(LiveMigrationConfiguration.class);
+ when(mockLiveMigrationConfiguration.filesToExclude())
+ .thenReturn(Collections.emptySet());
+ when(mockLiveMigrationConfiguration.directoriesToExclude())
+ .thenReturn(Collections.singleton("glob:${DATA_FILE_DIR}/*/*/snapshots"));
+ when(mockLiveMigrationConfiguration.migrationMap())
+ .thenReturn(Collections.emptyMap());
+
+ SidecarConfiguration sidecarConfiguration = SidecarConfigurationImpl.builder()
+ .liveMigrationConfiguration(mockLiveMigrationConfiguration)
+ .build();
+
+ bind(SidecarConfiguration.class).toInstance(sidecarConfiguration);
+ install(new HelperTestModules.InstanceMetadataTestModule(instanceMetaList));
+ }
+ }
+}
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
index 98fa8168..28297dc3 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
@@ -67,12 +67,12 @@ import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
import org.apache.cassandra.sidecar.modules.SidecarModules;
import org.apache.cassandra.sidecar.server.Server;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_FILES_API;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
import static org.apache.cassandra.sidecar.livemigration.InstanceFileInfoTestUtil.findInstanceFileInfo;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
index b77043c4..d3d58ee8 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
@@ -33,14 +33,14 @@ import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
import org.apache.cassandra.sidecar.common.response.InstanceFileInfo;
import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
import static org.apache.cassandra.sidecar.common.response.InstanceFileInfo.FileType.DIRECTORY;
import static org.apache.cassandra.sidecar.livemigration.InstanceFileInfoTestUtil.findInstanceFileInfo;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
index 6f8fb050..e5dc85b9 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
@@ -33,12 +33,12 @@ import org.junit.jupiter.api.io.TempDir;
import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
import org.mockito.Mockito;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.localPath;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
@@ -204,6 +204,58 @@ class LiveMigrationInstanceMetadataUtilTest
.doesNotContainKey(cassandraHomeDir + "/" + LOCAL_SYSTEM_DIR);
}
+ @Test
+ public void testPlaceholderDirsMap()
+ {
+ String cassandraHomeDir = tempDir.resolve("testPlaceholderDirsMap").toString();
+ InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+ List<String> dataDirs = new ArrayList<>(2);
+ String dataDir2 = DATA_DIR + "2";
+ dataDirs.add(cassandraHomeDir + "/" + DATA_DIR);
+ dataDirs.add(cassandraHomeDir + "/" + dataDir2);
+ when(instanceMetadata.dataDirs()).thenReturn(dataDirs);
+
+ Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+ Set<String> dirsToCopy = new HashSet<>(LiveMigrationInstanceMetadataUtil.dirsToCopy(instanceMetadata));
+ Set<String> dirsInMap = new HashSet<>();
+ placeholderDirsMap.forEach((k, v) -> dirsInMap.addAll(v));
+
+ // Placeholders should have been defined for all dirs that can be copied
+ assertThat(dirsInMap).isEqualTo(dirsToCopy);
+
+ assertThat(placeholderDirsMap).contains(
+ entry(HINTS_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.hintsDir())),
+ entry(COMMITLOG_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.commitlogDir())),
+ entry(SAVED_CACHES_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.savedCachesDir())),
+ entry(CDC_RAW_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.cdcDir())),
+ entry(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.localSystemDataFileDir())),
+ entry(DATA_FILE_DIR_PLACEHOLDER, new HashSet<>(instanceMetadata.dataDirs())),
+ entry(DATA_FILE_DIR_PLACEHOLDER + "_" + 0, Collections.singleton(instanceMetadata.dataDirs().get(0))),
+ entry(DATA_FILE_DIR_PLACEHOLDER + "_" + 1, Collections.singleton(instanceMetadata.dataDirs().get(1)))
+ );
+ }
+
+ @Test
+ public void testPlaceholderDirsMapFewDirsNotConfigured()
+ {
+ String cassandraHomeDir = tempDir.resolve("testPlaceholderDirsMapFewDirsNotConfigured").toString();
+ InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+ when(instanceMetadata.cdcDir()).thenReturn(null);
+ when(instanceMetadata.localSystemDataFileDir()).thenReturn(null);
+
+ Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+ Set<String> dirsToCopy = new HashSet<>(LiveMigrationInstanceMetadataUtil.dirsToCopy(instanceMetadata));
+ Set<String> dirsInMap = new HashSet<>();
+ placeholderDirsMap.forEach((k, v) -> dirsInMap.addAll(v));
+
+ // Placeholders should have been defined for all dirs that can be copied
+ assertThat(dirsInMap).isEqualTo(dirsToCopy);
+
+ assertThat(placeholderDirsMap).doesNotContainKey(CDC_RAW_DIR_PLACEHOLDER)
+ .doesNotContainKey(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER);
+ }
@Test
public void testLocalPath()
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
index e800dcf6..68480e25 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
@@ -18,16 +18,43 @@
package org.apache.cassandra.sidecar.livemigration;
+import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.mockito.Mockito;
+
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.DATA_FILE_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.HINTS_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.SAVED_CACHES_DIR_PLACEHOLDER;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.hasAnyPlaceholder;
import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.Mockito.when;
class LiveMigrationPlaceholderUtilTest
{
+
+ private static final String DATA_DIR = "data";
+ private static final String HINTS_DIR = "hints";
+ private static final String COMMITLOG_DIR = "commitlog";
+ private static final String SAVED_CACHES_DIR = "saved_caches";
+ private static final String CDC_RAW_DIR = "cdc_raw";
+ private static final String LOCAL_SYSTEM_DIR = "local_system_data";
+ private static final String STAGING_DIR = "sstable-staging";
+
+ @TempDir
+ Path tempDir;
+
@Test
public void testReplacePlaceholder()
{
@@ -70,4 +97,72 @@ class LiveMigrationPlaceholderUtilTest
assertThat(hasAnyPlaceholder("glob:${DATA_FILE_DIR}/ks/t1", Collections.singleton("DATA_FILE_DIR_0"))).isFalse();
}
+
+ @Test
+ public void testReplacePlaceholderUsingInstanceMetadata()
+ {
+ String cassandraHomeDir = tempDir.resolve("testReplacePlaceholderUsingInstanceMetadata").toString();
+ InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+ List<String> dataDirs = new ArrayList<>(2);
+ String dataDir2 = DATA_DIR + "2";
+ dataDirs.add(cassandraHomeDir + "/" + DATA_DIR);
+ dataDirs.add(cassandraHomeDir + "/" + dataDir2);
+ when(instanceMetadata.dataDirs()).thenReturn(dataDirs);
+
+ assertThat(replacePlaceholder("glob:${" + HINTS_DIR_PLACEHOLDER + "}", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.hintsDir());
+
+ assertThat(replacePlaceholder("regex:${" + COMMITLOG_DIR_PLACEHOLDER + "}/Commitlog-7-1.log", instanceMetadata))
+ .hasSize(1)
+ .contains("regex:" + instanceMetadata.commitlogDir() + "/Commitlog-7-1.log");
+
+ assertThat(replacePlaceholder("glob:${" + SAVED_CACHES_DIR_PLACEHOLDER + "}/cache.bin", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.savedCachesDir() + "/cache.bin");
+
+ assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "}/*/*/snapshots", instanceMetadata))
+ .hasSize(2)
+ .contains("glob:" + instanceMetadata.dataDirs().get(0) + "/*/*/snapshots")
+ .contains("glob:" + instanceMetadata.dataDirs().get(1) + "/*/*/snapshots");
+
+ assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "_0}/ks1/*", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.dataDirs().get(0) + "/ks1/*");
+
+ assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "_1}/k*/*", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.dataDirs().get(1) + "/k*/*");
+
+ assertThat(replacePlaceholder("glob:${" + CDC_RAW_DIR_PLACEHOLDER + "}/**", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.cdcDir() + "/**");
+
+ assertThat(replacePlaceholder("glob:${" + LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER + "}/", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:" + instanceMetadata.localSystemDataFileDir() + "/");
+
+ // Trying to replace text not having any placeholder
+ assertThat(replacePlaceholder("glob:/home/usr/data/*", instanceMetadata))
+ .hasSize(1)
+ .contains("glob:/home/usr/data/*");
+
+ //Unknown placeholder
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> replacePlaceholder("glob:${UNKNOWN_PLACE_HOLDER}/0", instanceMetadata));
+ }
+
+ InstanceMetadata getInstanceMetadata(String cassandraHomeDir)
+ {
+ InstanceMetadata instanceMetadata = Mockito.mock(InstanceMetadata.class);
+ when(instanceMetadata.dataDirs()).thenReturn(Collections.singletonList(cassandraHomeDir + "/" + DATA_DIR));
+ when(instanceMetadata.cdcDir()).thenReturn(cassandraHomeDir + "/" + CDC_RAW_DIR);
+ when(instanceMetadata.commitlogDir()).thenReturn(cassandraHomeDir + "/" + COMMITLOG_DIR);
+ when(instanceMetadata.hintsDir()).thenReturn(cassandraHomeDir + "/" + HINTS_DIR);
+ when(instanceMetadata.savedCachesDir()).thenReturn(cassandraHomeDir + "/" + SAVED_CACHES_DIR);
+ when(instanceMetadata.localSystemDataFileDir()).thenReturn(cassandraHomeDir + "/" + LOCAL_SYSTEM_DIR);
+ when(instanceMetadata.stagingDir()).thenReturn(cassandraHomeDir + "/" + STAGING_DIR);
+
+ return instanceMetadata;
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org