yifan-c commented on code in PR #239: URL: https://github.com/apache/cassandra-sidecar/pull/239#discussion_r2252811145
########## server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java: ########## Review Comment: Please revert the change. It is unrelated to the patch. ``` git checkout upstream/trunk -- server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java ``` I reverted the change and `gw :server:clean :server:spotbugsMain` passes. ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,246 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle openApiGenerate task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger logger = LoggerFactory.getLogger(OpenApiHandler.class); + private final OpenApiConfiguration openApiConfig; + + // Cache for both JSON and YAML specifications + private volatile String cachedJsonSpec; + private volatile String cachedYamlSpec; + private volatile long lastModified; Review Comment: I do not think we would modify the spec at the runtime. Can you remove this field and it should reduce the implementation complexity from assuming spec can change. ########## server/build.gradle: ########## @@ -201,6 +225,12 @@ test { apply from: "${project.rootDir}/gradle/common/integrationTestTask.gradle" apply from: "${project.rootDir}/gradle/common/java11Options.gradle" +tasks.withType(JavaCompile) { + doFirst { + println "AnnotationProcessorPath for $name is ${options.getAnnotationProcessorPath().getFiles()}" + } +} + Review Comment: This can be removed. I added it for debugging. Forgot to remove ########## OPENAPI.md: ########## Review Comment: The doc.. reads too AI. I think it only needs to cover the following points - what technologies are used for OpenAPI generation (i.e. the annotations and the plugin) - how to generate the openAPI spec - how to write the annotations in order to generate the API in the spec - what endpoints to access the specs (endpoints, e.g. /openapi.yaml) The rest like "architecture", client generation, migration guide are either unnecessary or wrong. They should be removed. Let's keep the doc succinct. ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,246 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle openApiGenerate task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger logger = LoggerFactory.getLogger(OpenApiHandler.class); + private final OpenApiConfiguration openApiConfig; + + // Cache for both JSON and YAML specifications + private volatile String cachedJsonSpec; + private volatile String cachedYamlSpec; + private volatile long lastModified; + + @Inject + public OpenApiHandler(SidecarConfiguration sidecarConfiguration) + { + this.openApiConfig = sidecarConfiguration.openApiConfiguration(); + } + + @Override + public void handle(RoutingContext context) + { + try + { + // Determine format from request path or Accept header + boolean isYaml = isYamlRequest(context); + String openApiContent = loadGeneratedOpenApiSpec(isYaml); + String contentType = isYaml ? "application/yaml" : "application/json"; + + context.response() + .putHeader("Content-Type", contentType) + .end(openApiContent); + } + catch (Exception e) + { + logger.warn("Failed to load generated OpenAPI specification, falling back to basic config", e); + + // Fallback to basic OpenAPI config if generated file is not available + OpenAPI openAPI = createOpenApiFromConfig(openApiConfig); + String openApiJson = Json.pretty(openAPI); + + context.response() + .putHeader("Content-Type", "application/json") + .end(openApiJson); + } + } + + /** + * Determines if the request is for YAML format based on path or Accept header + */ + private boolean isYamlRequest(RoutingContext context) + { + String path = context.request().path(); + if (path != null && path.endsWith(".yaml")) + { + return true; + } + + String acceptHeader = context.request().getHeader("Accept"); + return acceptHeader != null && acceptHeader.contains("application/yaml"); + } + + /** + * Loads the generated OpenAPI specification from resources or build output with caching + */ + private String loadGeneratedOpenApiSpec(boolean isYaml) throws IOException + { + // First try to load from classpath resources (for packaged deployments) + String cached = isYaml ? cachedYamlSpec : cachedJsonSpec; + if (cached == null) + { + cached = loadFromClasspathResource(isYaml); + if (isYaml) + { + cachedYamlSpec = cached; + } + else + { + cachedJsonSpec = cached; + } + } + + if (cached != null) + { + return cached; + } + + // Fallback to file system paths (for development) + return loadFromFileSystem(isYaml); + } + + /** + * Attempts to load the OpenAPI spec from classpath resources + */ + private String loadFromClasspathResource(boolean isYaml) throws IOException + { + String fileName = isYaml ? "openapi.yaml" : "openapi.json"; + String resourcePath = "/openapi/" + fileName; + InputStream inputStream = getClass().getResourceAsStream(resourcePath); + if (inputStream == null) + { + return null; + } + + try + { + logger.debug("Loading OpenAPI specification from classpath: {}", resourcePath); + String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + logger.info("Loaded OpenAPI specification from classpath resource"); + return content; + } + finally + { + inputStream.close(); + } + } + + /** + * Attempts to load the OpenAPI spec from file system (development mode) + */ + private String loadFromFileSystem(boolean isYaml) throws IOException + { + String fileName = isYaml ? "openapi.yaml" : "openapi.json"; + String[] possiblePaths = { + "server/build/generated/openapi/" + fileName, // From project root + "build/generated/openapi/" + fileName, // From server directory + "../server/build/generated/openapi/" + fileName, // Alternative relative path + }; + + Path foundPath = null; + for (String pathStr : possiblePaths) + { + Path path = Paths.get(pathStr); + if (Files.exists(path)) + { + foundPath = path; + break; + } + } + + if (foundPath == null) + { + throw new IOException("Generated OpenAPI specification not found in classpath resources or file system. " + + "Please run './gradlew openApiGenerate' and ensure the generated file is included in the build."); + } + + // Check if we need to reload the file (if it's newer than our cached version) + long fileLastModified = Files.getLastModifiedTime(foundPath).toMillis(); + String cached = isYaml ? cachedYamlSpec : cachedJsonSpec; + if (cached == null || fileLastModified > lastModified) + { + logger.debug("Loading OpenAPI specification from: {}", foundPath.toAbsolutePath()); + String content = Files.readString(foundPath, StandardCharsets.UTF_8); + lastModified = fileLastModified; + logger.info("Loaded OpenAPI specification from {}", foundPath.toAbsolutePath()); + + // Update the appropriate cache + if (isYaml) + { + cachedYamlSpec = content; + } + else + { + cachedJsonSpec = content; + } + + return content; + } + + return cached; + } + + /** + * Creates a basic OpenAPI configuration as fallback when generated spec is not available + */ + private static OpenAPI createOpenApiFromConfig(OpenApiConfiguration config) + { + OpenAPI openApi = new OpenAPI(); + + // Set basic info + io.swagger.v3.oas.models.info.Info info = new io.swagger.v3.oas.models.info.Info(); + info.setTitle(config.title()); + info.setDescription(config.description() + " (Generated specification not available - run './gradlew openApiGenerate')"); + info.setVersion(config.version()); + + // Set license info + io.swagger.v3.oas.models.info.License license = new io.swagger.v3.oas.models.info.License(); + license.setName(config.licenseName()); + license.setUrl(config.licenseUrl()); + info.setLicense(license); + + openApi.setInfo(info); + + // Set server info + io.swagger.v3.oas.models.servers.Server server = new io.swagger.v3.oas.models.servers.Server(); + server.setUrl(config.serverUrl()); + server.setDescription(config.serverDescription()); + openApi.setServers(Collections.singletonList(server)); + + return openApi; + } Review Comment: There is no use of this fallback. Can we just return a 404 if no spec is available? ########## server/build.gradle: ########## @@ -240,3 +273,22 @@ checkstyleContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } spotbugsContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } check.dependsOn containerTest, integrationTest, jacocoTestReport + +// Configure OpenAPI generation to work with packaged deployments +// Copy generated OpenAPI spec to resources build directory so it's available in the JAR +tasks.register('copyOpenApiSpec', Copy) { + dependsOn generateOpenApiSpec + from layout.buildDirectory.dir('generated/openapi') + include 'openapi.json', 'openapi.yaml' + into layout.buildDirectory.dir('resources/main/openapi') + doFirst { + mkdir layout.buildDirectory.dir('resources/main/openapi') + } +} + +// Ensure the OpenAPI spec is copied before building the JAR +processResources.dependsOn copyOpenApiSpec + +// OpenAPI generation now uses standard JAX-RS/MicroProfile annotations +// and can be generated using external tools like the swagger-codegen plugin +// or the official MicroProfile OpenAPI Maven/Gradle plugins Review Comment: This comment can be removed. In the git history of trunk, the openAPI spec generation is new. There is no prior implementation, which makes the comment useless. ########## server/build.gradle: ########## @@ -232,6 +262,9 @@ jib { to { image = "cassandra-sidecar:${project.version}" } + container { + mainClass = 'org.apache.cassandra.sidecar.CassandraSidecarDaemon' + } Review Comment: I am not sure why this is needed. To run the application, the `run` task is defined in `build.gradle` at the project root already. ########## server/src/main/java/org/apache/cassandra/sidecar/modules/OpenApiModule.java: ########## @@ -0,0 +1,94 @@ +/* + * 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.modules; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.ProvidesIntoMap; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.apache.cassandra.sidecar.common.ApiEndpointsV1; +import org.apache.cassandra.sidecar.handlers.OpenApiHandler; +import org.apache.cassandra.sidecar.handlers.SwaggerUIHandler; +import org.apache.cassandra.sidecar.modules.multibindings.KeyClassMapKey; +import org.apache.cassandra.sidecar.modules.multibindings.VertxRouteMapKeys; +import org.apache.cassandra.sidecar.routes.RouteBuilder; +import org.apache.cassandra.sidecar.routes.VertxRoute; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +/** + * Module for OpenAPI documentation + */ +@Path("/") +public class OpenApiModule extends AbstractModule +{ + @GET + @Path(ApiEndpointsV1.OPENAPI_ROUTE) + @Operation(summary = "Get OpenAPI specification", + description = "Returns the OpenAPI specification for the Cassandra Sidecar API") + @APIResponse(description = "OpenAPI specification retrieved successfully", + responseCode = "200", + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT))) + @ProvidesIntoMap + @KeyClassMapKey(VertxRouteMapKeys.OpenApiRouteKey.class) + VertxRoute openApiRoute(RouteBuilder.Factory factory, OpenApiHandler openApiHandler) + { + return factory.builderForUnauthorizedRoute() Review Comment: Should the openapi spec be placed behind the unauthorized route? I would like to hear from @sarankk and @frankgh. I guess one argument of not having auth is that client can easily get the spec from the github. ########## server/build.gradle: ########## @@ -240,3 +273,22 @@ checkstyleContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } spotbugsContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } check.dependsOn containerTest, integrationTest, jacocoTestReport + +// Configure OpenAPI generation to work with packaged deployments +// Copy generated OpenAPI spec to resources build directory so it's available in the JAR +tasks.register('copyOpenApiSpec', Copy) { + dependsOn generateOpenApiSpec + from layout.buildDirectory.dir('generated/openapi') + include 'openapi.json', 'openapi.yaml' + into layout.buildDirectory.dir('resources/main/openapi') + doFirst { + mkdir layout.buildDirectory.dir('resources/main/openapi') + } +} Review Comment: I think you can configure `outputDirectory` for the `generateOpenApiSpec` task. Then, you can remove this task. It is not very well documented. You can find all properties here https://github.com/smallrye/smallrye-open-api/tree/main/tools/maven-plugin ########## server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java: ########## @@ -192,6 +193,21 @@ interface ListSnapshotRouteKey extends RouteClassKey HttpMethod HTTP_METHOD = HttpMethod.GET; String ROUTE_URI = ApiEndpointsV1.SNAPSHOTS_ROUTE; } + interface OpenApiRouteKey extends RouteClassKey + { + HttpMethod HTTP_METHOD = HttpMethod.GET; + String ROUTE_URI = ApiEndpointsV1.OPENAPI_ROUTE; + } + interface OpenApiYamlRouteKey extends RouteClassKey + { + HttpMethod HTTP_METHOD = HttpMethod.GET; + String ROUTE_URI = ApiEndpointsV1.OPENAPI_YAML_ROUTE; + } + interface SwaggerUIRouteKey extends RouteClassKey Review Comment: `OpenApiHtmlRouteKey`? ########## client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java: ########## @@ -157,6 +157,10 @@ public final class ApiEndpointsV1 public static final String LIVE_MIGRATION_FILE_TRANSFER_API = LIVE_MIGRATION_FILES_API + "/:" + DIR_TYPE_PARAM + "/:" + DIR_INDEX_PARAM + "/*"; + public static final String OPENAPI_ROUTE = API_V1 + "/openapi.json"; + public static final String OPENAPI_YAML_ROUTE = API_V1 + "/openapi.yaml"; + public static final String SWAGGER_UI_ROUTE = API_V1 + "/docs"; Review Comment: The data being served from those endpoints can contain multiple api versions. It would make better sense to have it at the root level. For example, `localhost:9043/openapi{.type}`. Nit: instead of `docs`, how about `openapi.html`? ########## OPENAPI.md: ########## @@ -0,0 +1,173 @@ +<!-- +# +# 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. +# +--> + +# OpenAPI Documentation + +This project uses standard JAX-RS and MicroProfile OpenAPI annotations to generate comprehensive API documentation for the Cassandra Sidecar. + +## Architecture Overview + +### Standard Annotations Approach +The Cassandra Sidecar uses industry-standard annotations for OpenAPI documentation generation: + +- **JAX-RS Annotations**: `@GET`, `@POST`, `@PUT`, `@DELETE`, `@Path` - Define HTTP methods and routes +- **MicroProfile OpenAPI Annotations**: `@Operation`, `@APIResponse`, `@Schema` - Add comprehensive API documentation +- **SmallRye OpenAPI Plugin**: Gradle plugin that processes annotations to generate OpenAPI specifications Review Comment: please add links to the annotations and the plugin ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,246 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle openApiGenerate task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger logger = LoggerFactory.getLogger(OpenApiHandler.class); + private final OpenApiConfiguration openApiConfig; + + // Cache for both JSON and YAML specifications + private volatile String cachedJsonSpec; + private volatile String cachedYamlSpec; + private volatile long lastModified; + + @Inject + public OpenApiHandler(SidecarConfiguration sidecarConfiguration) + { + this.openApiConfig = sidecarConfiguration.openApiConfiguration(); + } + + @Override + public void handle(RoutingContext context) + { + try + { + // Determine format from request path or Accept header + boolean isYaml = isYamlRequest(context); + String openApiContent = loadGeneratedOpenApiSpec(isYaml); + String contentType = isYaml ? "application/yaml" : "application/json"; + + context.response() + .putHeader("Content-Type", contentType) + .end(openApiContent); + } + catch (Exception e) + { + logger.warn("Failed to load generated OpenAPI specification, falling back to basic config", e); + + // Fallback to basic OpenAPI config if generated file is not available + OpenAPI openAPI = createOpenApiFromConfig(openApiConfig); + String openApiJson = Json.pretty(openAPI); + + context.response() + .putHeader("Content-Type", "application/json") + .end(openApiJson); + } + } + + /** + * Determines if the request is for YAML format based on path or Accept header + */ + private boolean isYamlRequest(RoutingContext context) + { + String path = context.request().path(); + if (path != null && path.endsWith(".yaml")) + { + return true; + } + + String acceptHeader = context.request().getHeader("Accept"); + return acceptHeader != null && acceptHeader.contains("application/yaml"); + } + + /** + * Loads the generated OpenAPI specification from resources or build output with caching + */ + private String loadGeneratedOpenApiSpec(boolean isYaml) throws IOException + { + // First try to load from classpath resources (for packaged deployments) + String cached = isYaml ? cachedYamlSpec : cachedJsonSpec; + if (cached == null) + { + cached = loadFromClasspathResource(isYaml); + if (isYaml) + { + cachedYamlSpec = cached; + } + else + { + cachedJsonSpec = cached; + } + } + + if (cached != null) + { + return cached; + } + + // Fallback to file system paths (for development) + return loadFromFileSystem(isYaml); + } + + /** + * Attempts to load the OpenAPI spec from classpath resources + */ + private String loadFromClasspathResource(boolean isYaml) throws IOException + { + String fileName = isYaml ? "openapi.yaml" : "openapi.json"; + String resourcePath = "/openapi/" + fileName; + InputStream inputStream = getClass().getResourceAsStream(resourcePath); + if (inputStream == null) + { + return null; + } + + try + { + logger.debug("Loading OpenAPI specification from classpath: {}", resourcePath); + String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + logger.info("Loaded OpenAPI specification from classpath resource"); + return content; + } + finally + { + inputStream.close(); + } Review Comment: nit: use try-with-resources. ```suggestion try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) { if (inputStream == null) { return null; } logger.debug("Loading OpenAPI specification from classpath: {}", resourcePath); String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); logger.info("Loaded OpenAPI specification from classpath resource"); return content; } ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For additional commands, e-mail: pr-h...@cassandra.apache.org