jyothsnakonisa commented on code in PR #272: URL: https://github.com/apache/cassandra-sidecar/pull/272#discussion_r2624587881
########## adapters/adapters-base/src/test/java/org/apache/cassandra/sidecar/adapters/base/CassandraCompactionManagerOperationsTest.java: ########## @@ -0,0 +1,127 @@ +/* + * 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.adapters.base; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.apache.cassandra.sidecar.adapters.base.jmx.CompactionManagerJmxOperations; +import org.apache.cassandra.sidecar.common.server.JmxClient; + +import static org.apache.cassandra.sidecar.adapters.base.jmx.CompactionManagerJmxOperations.COMPACTION_MANAGER_OBJ_NAME; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link CassandraCompactionManagerOperations} class + */ +class CassandraCompactionManagerOperationsTest +{ + private CassandraCompactionManagerOperations compactionManagerOperations; + private JmxClient mockJmxClient; + private CompactionManagerJmxOperations mockJmxOperations; + + @BeforeEach + void setUp() + { + mockJmxClient = mock(JmxClient.class); + mockJmxOperations = mock(CompactionManagerJmxOperations.class); + compactionManagerOperations = new CassandraCompactionManagerOperations(mockJmxClient); + + // Setup JMX proxy mock + when(mockJmxClient.proxy(CompactionManagerJmxOperations.class, COMPACTION_MANAGER_OBJ_NAME)) + .thenReturn(mockJmxOperations); + } + + @Test + void testStopCompactionByIdOnly() + { + // Test stopCompactionById called when providing compactionId + String compactionId = "abc-123"; + compactionManagerOperations.stopCompaction(compactionId); + + verify(mockJmxOperations, times(1)).stopCompactionById(compactionId); + verify(mockJmxOperations, times(0)).stopCompaction(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + void testStopCompactionByTypeOnly() + { + // Test stopCompaction called when no compactionId provided + String compactionType = "COMPACTION"; + compactionManagerOperations.stopCompaction(compactionType); + + verify(mockJmxOperations, times(1)).stopCompaction(compactionType); + verify(mockJmxOperations, times(0)).stopCompactionById(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + void testStopCompactionBothProvided() + { + // Test compactionId takes precedence when both compactionId and compactionType provided + String compactionId = "xyz-456"; + String compactionType = "VALIDATION"; + compactionManagerOperations.stopCompaction(compactionType); + + verify(mockJmxOperations, times(1)).stopCompactionById(compactionId); + verify(mockJmxOperations, times(0)).stopCompaction(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + void testStopCompactionByIdWithWhitespace() + { + // Test trim does not result in empty string + String compactionId = " abc-123 "; + compactionManagerOperations.stopCompaction(compactionId); + + verify(mockJmxOperations, times(1)).stopCompactionById(compactionId); + verify(mockJmxOperations, times(0)).stopCompaction(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + void testStopCompactionAllSupportedTypes() + { + // Test no failures upon any supported type being provided as param + String[] supportedTypes = { + "COMPACTION", "VALIDATION", "KEY_CACHE_SAVE", "ROW_CACHE_SAVE", + "COUNTER_CACHE_SAVE", "CLEANUP", "SCRUB", "UPGRADE_SSTABLES", + "INDEX_BUILD", "TOMBSTONE_COMPACTION", "UNKNOWN", "ANTICOMPACTION", + "VERIFY", "VIEW_BUILD", "INDEX_SUMMARY", "RELOCATE", + "GARBAGE_COLLECT", "WRITE" + }; + + for (String type : supportedTypes) + { + compactionManagerOperations.stopCompaction(type); + verify(mockJmxOperations, times(1)).stopCompaction(type); + } + } + + @Test + void testStopCompactionJmxProxyCalledOnce() + { + // Test JMX proxy obtained exactly once per call + compactionManagerOperations.stopCompactionById("test-id"); + + verify(mockJmxClient, times(1)) + .proxy(CompactionManagerJmxOperations.class, COMPACTION_MANAGER_OBJ_NAME); + } +} Review Comment: Please add a new line in the end of the file ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/CompactionStopHandler.java: ########## @@ -0,0 +1,168 @@ +/* + * 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.util.Collections; +import java.util.Set; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.common.data.CompactionStopStatus; +import org.apache.cassandra.sidecar.common.data.CompactionType; +import org.apache.cassandra.sidecar.common.request.data.CompactionStopRequestPayload; +import org.apache.cassandra.sidecar.common.response.CompactionStopResponse; +import org.apache.cassandra.sidecar.common.server.CompactionManagerOperations; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; + +import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException; + +/** + * Handler for stopping compaction operations via the Cassandra Compaction Manager. + * + * <p>Handles {@code POST /api/v1/cassandra/operations/compaction/stop} requests to stop + * compaction operations. Expects a JSON payload with compaction_type and/or compaction_id: + * <pre> + * { "compaction_type": "COMPACTION", "compaction_id": "abc-123" } + * </pre> + */ +public class CompactionStopHandler extends AbstractHandler<CompactionStopRequestPayload> implements AccessProtected +{ + /** + * Constructs a handler with the provided {@code metadataFetcher} + * + * @param metadataFetcher the metadata fetcher + * @param executorPools executor pools for blocking executions + * @param validator validator for Cassandra-specific input + */ + @Inject + protected CompactionStopHandler(final InstanceMetadataFetcher metadataFetcher, + final ExecutorPools executorPools, + final CassandraInputValidator validator) { + super(metadataFetcher, executorPools, validator); + } + + @Override + public Set<Authorization> requiredAuthorizations() { + return Collections.singleton(BasicPermissions.MODIFY_COMPACTION.toAuthorization()); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleInternal(RoutingContext context, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + CompactionStopRequestPayload request) { + CompactionManagerOperations compactionManagerOps = metadataFetcher.delegate(host).compactionManagerOperations(); + + executorPools.service() + .executeBlocking(() -> stopCompaction(compactionManagerOps, request)) + .onSuccess(context::json) + .onFailure(cause -> processFailure(cause, context, host, remoteAddress, request)); + } + + /** + * Stops the compaction based on the request parameters + * + * @param operations the compaction manager operations + * @param request the request payload containing compaction_type and/or compaction_id + * @return CompactionStopResponse with the operation result + */ + private CompactionStopResponse stopCompaction(CompactionManagerOperations operations, + CompactionStopRequestPayload request) + { + CompactionType compactionType = request.compactionType(); + String compactionId = request.compactionId(); Review Comment: Do you want to trim the compactionID? I see that you are doing it in the other parts of the code ########## client-common/src/test/java/org/apache/cassandra/sidecar/common/request/data/CompactionStopRequestPayloadTest.java: ########## @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.sidecar.common.request.data; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.cassandra.sidecar.common.data.CompactionType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link CompactionStopRequestPayload} serialization and deserialization + */ +class CompactionStopRequestPayloadTest +{ + private static final ObjectMapper MAPPER = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + @Test + void testSerDeserWithBothFields() throws JsonProcessingException + { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(CompactionType.COMPACTION, "abc-123"); + String json = MAPPER.writeValueAsString(payload); + assertThat(json).isEqualTo("{\"compaction_type\":\"COMPACTION\",\"compaction_id\":\"abc-123\"}"); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isEqualTo(payload.compactionType()); + assertThat(deser.compactionId()).isEqualTo(payload.compactionId()); + } + + @Test + void testSerDeserWithTypeOnly() throws JsonProcessingException + { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(CompactionType.VALIDATION, null); + String json = MAPPER.writeValueAsString(payload); + assertThat(json).isEqualTo("{\"compaction_type\":\"VALIDATION\"}"); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isEqualTo(CompactionType.VALIDATION); + assertThat(deser.compactionId()).isNull(); + } + + @Test + void testSerDeserWithIdOnly() throws JsonProcessingException + { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(null, "xyz-456"); + String json = MAPPER.writeValueAsString(payload); + assertThat(json).isEqualTo("{\"compaction_id\":\"xyz-456\"}"); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isNull(); + assertThat(deser.compactionId()).isEqualTo("xyz-456"); + } + + @Test + void testSerDeserWithBothNull() throws JsonProcessingException + { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(null, null); + String json = MAPPER.writeValueAsString(payload); + assertThat(json).isEqualTo("{}"); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isNull(); + assertThat(deser.compactionId()).isNull(); + } + + @Test + void testDeserFromJsonWithBothFields() throws JsonProcessingException + { + String json = "{\"compaction_type\":\"CLEANUP\",\"compaction_id\":\"test-123\"}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isEqualTo(CompactionType.CLEANUP); + assertThat(payload.compactionId()).isEqualTo("test-123"); + } + + @Test + void testDeserFromJsonWithTypeOnly() throws JsonProcessingException + { + String json = "{\"compaction_type\":\"SCRUB\"}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isEqualTo(CompactionType.SCRUB); + assertThat(payload.compactionId()).isNull(); + } + + @Test + void testDeserFromJsonWithIdOnly() throws JsonProcessingException + { + String json = "{\"compaction_id\":\"unique-compaction-id\"}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isNull(); + assertThat(payload.compactionId()).isEqualTo("unique-compaction-id"); + } + + @Test + void testDeserFromEmptyJson() throws JsonProcessingException + { + String json = "{}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isNull(); + assertThat(payload.compactionId()).isNull(); + } + + @Test + void testDeserializeWithEmptyStrings() throws JsonProcessingException + { + String json = "{\"compaction_type\":\"\",\"compaction_id\":\"\"}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isNull(); + assertThat(payload.compactionId()).isEmpty(); + } + + @Test + void testDeserializeWithWhitespace() throws JsonProcessingException + { + String json = "{\"compaction_type\":\" COMPACTION \",\"compaction_id\":\" test-id \"}"; + CompactionStopRequestPayload payload = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(payload.compactionType()).isEqualTo(CompactionType.COMPACTION); + assertThat(payload.compactionId()).isEqualTo(" test-id "); + } + + @Test + void testToString() + { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(CompactionType.COMPACTION, "abc-123"); + String toString = payload.toString(); + assertThat(toString).contains("compaction"); + assertThat(toString).contains("abc-123"); + assertThat(toString).contains("CompactionStopRequestPayload"); + } + + @Test + void testAllSupportedCompactionTypes() throws JsonProcessingException + { + + // Check each compactionType field for CompactionStopRequestPayload is recognized as a supported compaction type + for (CompactionType type : CompactionType.values()) { + CompactionStopRequestPayload payload = new CompactionStopRequestPayload(type, null); + String json = MAPPER.writeValueAsString(payload); + assertThat(json).contains(type.toString().toUpperCase()); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isEqualTo(type); + } + } + + @Test + void testCasePreservation() throws JsonProcessingException + { + // Test preserved in serialization/deserialization + CompactionStopRequestPayload lowerCase = new CompactionStopRequestPayload(CompactionType.COMPACTION, "Test-ID-123"); + String json = MAPPER.writeValueAsString(lowerCase); + assertThat(json).contains("compaction"); + assertThat(json).contains("Test-ID-123"); + + CompactionStopRequestPayload deser = MAPPER.readValue(json, CompactionStopRequestPayload.class); + assertThat(deser.compactionType()).isEqualTo(CompactionType.COMPACTION); + assertThat(deser.compactionId()).isEqualTo("Test-ID-123"); + } +} Review Comment: Please add a new line at the end of the file ########## adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraCompactionManagerOperations.java: ########## @@ -53,4 +55,31 @@ public List<Map<String, String>> getCompactions() return jmxClient.proxy(CompactionManagerJmxOperations.class, COMPACTION_MANAGER_OBJ_NAME) .getCompactions(); } + + /** + * {@inheritDoc} + */ + @Override + public void stopCompactionById(String compactionId) + { + // compactionId takes precedence over type if both are provided + if (compactionId != null && !compactionId.trim().isEmpty()) { + CompactionManagerJmxOperations proxy = jmxClient.proxy( + CompactionManagerJmxOperations.class, + COMPACTION_MANAGER_OBJ_NAME + ); + proxy.stopCompactionById(compactionId); + } Review Comment: Do you want to throw an exception when the compactionId is invalid here and in the `stopCompaction` method ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/CompactionStopHandler.java: ########## @@ -0,0 +1,168 @@ +/* + * 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.util.Collections; +import java.util.Set; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.common.data.CompactionStopStatus; +import org.apache.cassandra.sidecar.common.data.CompactionType; +import org.apache.cassandra.sidecar.common.request.data.CompactionStopRequestPayload; +import org.apache.cassandra.sidecar.common.response.CompactionStopResponse; +import org.apache.cassandra.sidecar.common.server.CompactionManagerOperations; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; + +import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException; + +/** + * Handler for stopping compaction operations via the Cassandra Compaction Manager. + * + * <p>Handles {@code POST /api/v1/cassandra/operations/compaction/stop} requests to stop + * compaction operations. Expects a JSON payload with compaction_type and/or compaction_id: + * <pre> + * { "compaction_type": "COMPACTION", "compaction_id": "abc-123" } + * </pre> + */ +public class CompactionStopHandler extends AbstractHandler<CompactionStopRequestPayload> implements AccessProtected +{ + /** + * Constructs a handler with the provided {@code metadataFetcher} + * + * @param metadataFetcher the metadata fetcher + * @param executorPools executor pools for blocking executions + * @param validator validator for Cassandra-specific input + */ + @Inject + protected CompactionStopHandler(final InstanceMetadataFetcher metadataFetcher, + final ExecutorPools executorPools, + final CassandraInputValidator validator) { + super(metadataFetcher, executorPools, validator); + } + + @Override + public Set<Authorization> requiredAuthorizations() { + return Collections.singleton(BasicPermissions.MODIFY_COMPACTION.toAuthorization()); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleInternal(RoutingContext context, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + CompactionStopRequestPayload request) { + CompactionManagerOperations compactionManagerOps = metadataFetcher.delegate(host).compactionManagerOperations(); + + executorPools.service() + .executeBlocking(() -> stopCompaction(compactionManagerOps, request)) + .onSuccess(context::json) + .onFailure(cause -> processFailure(cause, context, host, remoteAddress, request)); + } + + /** + * Stops the compaction based on the request parameters + * + * @param operations the compaction manager operations + * @param request the request payload containing compaction_type and/or compaction_id + * @return CompactionStopResponse with the operation result + */ + private CompactionStopResponse stopCompaction(CompactionManagerOperations operations, + CompactionStopRequestPayload request) + { + CompactionType compactionType = request.compactionType(); + String compactionId = request.compactionId(); + + // Convert enum to string for the operation (if not null) + // Use .name() to get uppercase enum name, matching Cassandra's OperationType enum format + String compactionTypeStr = compactionType != null ? compactionType.name() : null; + + // Attempt to stop the compaction + // If compactionId is provided, use it (takes precedence over type) + if (request.hasValidCompactionId()) { + operations.stopCompactionById(compactionId); + } else if (request.hasValidCompactionType()) { + operations.stopCompaction(compactionTypeStr); + } + // If we reach here, at least one of the above conditions was true due to validation in extractParamsOrThrow() + // Return success response + return CompactionStopResponse.builder() + .compactionType(compactionType) + .compactionId(compactionId) + .status(CompactionStopStatus.SUBMITTED) + .build(); + } + + /** + * Override extractParamsOrThrow to support compactionStop param constraints + * Method extracts and validates compaction stop request from routing context + * + * @param context the routing context + * @return validated CompactionStopRequestPayload + */ + @Override + protected CompactionStopRequestPayload extractParamsOrThrow(RoutingContext context) + { + String body = context.body().asString(); + CompactionStopRequestPayload payload; + + /* + Return 400 BAD_REQUEST upon malformed JSON - avoids falling under processFailure()'s vague + 500 INTERNAL_SERVER_ERROR. Also catches invalid compaction types via CompactionType.fromString() + */ + try + { + payload = Json.decodeValue(body, CompactionStopRequestPayload.class); + } + catch (DecodeException e) + { + logger.warn("Bad request. Received invalid JSON payload: {}", e.getMessage()); + throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, + "Invalid JSON payload: " + e.getMessage()); + } + catch (IllegalArgumentException e) + { + logger.warn("Bad request. {}", e.getMessage()); + throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, e.getMessage()); + } + + // Validate that at least one field is provided + if (!payload.atLeastOneParamProvided()) + { + logger.warn("Bad request. Both compaction_type and compaction_id are missing."); + throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, + "At least one of 'compaction_type' or 'compaction_id' must be provided"); + } + + return payload; + } +} Review Comment: Please add a new line here -- 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: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]

