You are viewing a plain text version of this content. The canonical link for it is here.
Posted to pr@cassandra.apache.org by "andresbeckruiz (via GitHub)" <gi...@apache.org> on 2025/08/27 21:30:49 UTC
[PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
andresbeckruiz opened a new pull request, #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256
[CASSSIDECAR-266](https://issues.apache.org/jira/browse/CASSSIDECAR-266): This patch adds the following Lifecycle APIs for starting and stopping Cassandra, as well as retrieving the current lifecycle information of a local Cassandra host:
`PUT /api/v1/cassandra/lifecycle {"state": "<start|stop>"}`
`GET /api/v1/cassandra/lifecycle`
This patch implements the `InJVMDLifecycleProvider` for Jvm DTests, and provides a stencil of a `ProccessLifecycleProvider` for managing Cassandra instances that are hosted as OS processes. In order to make review more manageable, this provider will be implemented and tested in a separate patch. This patch will also contain a new lifecycle section in sidecar.yaml to allow for pluggable lifecycle providers (which can implement the generic `LifecycleProvider` interface).
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2383330608
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleStatus.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+/**
+ * Represents the lifecycle status of this instance relative to the desired state.
+ */
+public enum LifecycleStatus
+{
+ /**
+ * The status when a desired lifecycle state has not been submitted yet
+ */
+ UNDEFINED,
+
+ /**
+ * The status when the current lifecycle state of an instance matches the desired lifecycle state
+ */
+ CONVERGED,
+
+ /**
+ * The status when the current lifecycle state of an instance does not match the desired lifecycle state
+ */
+ DIVERGED,
+
+ /**
+ * The status when an instance is starting or stopping to match the desired lifecycle state
+ */
+ CONVERGING;
Review Comment:
'CONVERGING' occurs when an operation is in progress to match the states, 'DIVERGED' occurs when there is no operation in progress.
I've added more detail to the comments to distinguish these two statuses.
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2353599842
##########
server/src/main/java/org/apache/cassandra/sidecar/config/yaml/LifecycleConfigurationImpl.java:
##########
@@ -0,0 +1,70 @@
+/*
+ * 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.config.yaml;
+
+import java.util.Collections;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+
+/**
+ * Configuration for Cassandra lifecycle management service
+ */
+public class LifecycleConfigurationImpl implements LifecycleConfiguration
+{
+ private static final boolean DEFAULT_ENABLED = false;
+ private static final ParameterizedClassConfiguration DEFAULT_LIFECYCLE_PROVIDER =
+ new ParameterizedClassConfigurationImpl(ProcessLifecycleProvider.class.getName(), Collections.emptyMap());
+
+ protected final Boolean enabled;
+
+ protected final String directory;
Review Comment:
Yes, this will be used in the `ProcessLifecycleProvider` implemented in [CASSSIDECAR-340](https://issues.apache.org/jira/browse/CASSSIDECAR-340?orderby=priority+ASC%2C+updated+DESC)
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2353600937
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/LifecycleTaskInfo.java:
##########
@@ -0,0 +1,58 @@
+/*
+ * 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.lifecycle;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a lifecycle task
+ */
+public class LifecycleTaskInfo
Review Comment:
This is not needed anymore, thanks for pointing out. Removed
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2353598877
##########
server/src/main/java/org/apache/cassandra/sidecar/config/LifecycleConfiguration.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.config;
+
+/**
+ * Configuration for Cassandra lifecycle management service
+ */
+public interface LifecycleConfiguration
Review Comment:
Was planning to add the `sidecar.yaml` configuration and documentation as part of [CASSSIDECAR-340](https://issues.apache.org/jira/browse/CASSSIDECAR-340?orderby=priority+ASC%2C+updated+DESC) once the `ProcessLifecycleProvider` is implemented
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "frankgh (via GitHub)" <gi...@apache.org>.
frankgh commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2364743140
##########
integration-framework/src/main/java/org/apache/cassandra/sidecar/lifecycle/InJvmDTestLifecycleProvider.java:
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of JVM Dtest Cassandra instances.
+ * This should used for integration tests where Cassandra instances are started and stopped
Review Comment:
minor NIT
```suggestion
* This should be used for integration tests where Cassandra instances are started and stopped
```
##########
integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/InJvmLifecycleProviderIntegrationTest.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.lifecycle;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
+import org.apache.cassandra.testing.ClusterBuilderConfiguration;
+
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+/**
+ * Tests the {@link InJvmDTestLifecycleProvider} to ensure it can start and stop a Cassandra node
+ */
+public class InJvmLifecycleProviderIntegrationTest extends SharedClusterSidecarIntegrationTestBase
+{
+ static final InstanceMetadata LOCALHOST_METADATA = mock(InstanceMetadata.class);
+ private static final String JVM_LIFECYCLE_TEST_MIN_VERSION = "4.1";
+
+ @BeforeAll
+ static void beforeAll()
+ {
+ when(LOCALHOST_METADATA.host()).thenReturn("localhost");
+ }
+
Review Comment:
can we do the version check _before_ we provision the cluster? this will help speed up tests
```
@Override
protected void beforeClusterProvisioning()
{
// JVM Distributed Test framework contains a bug with restarting nodes in version 4.0 (CASSANDRA-19729)
assumeThat(SimpleCassandraVersion.create(testVersion.version()))
.withFailMessage("JVM Distributed Test framework contains a bug with restarting nodes in version 4.0 (CASSANDRA-19729)")
.isGreaterThanOrEqualTo(SimpleCassandraVersion.create(JVM_LIFECYCLE_TEST_MIN_VERSION));
}
```
##########
server/src/test/java/org/apache/cassandra/sidecar/lifecycle/LifecycleManagerTest.java:
##########
@@ -0,0 +1,300 @@
+/*
+ * 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.lifecycle;
+
+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 io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link LifecycleManager} to validate lifecycle management behavior
+ */
+class LifecycleManagerTest
+{
+ private static final String TEST_HOST = "127.0.0.1";
+ private static final InstanceMetadata TEST_HOST_META = mock(InstanceMetadata.class);
+ private static final String TEST_HOST_2 = "127.0.0.2";
+ private static final InstanceMetadata TEST_HOST_2_META = mock(InstanceMetadata.class);
+
+ protected Vertx vertx;
+ protected ExecutorPools executorPools;
+ protected LifecycleProvider mockLifecycleProvider;
+ protected InstanceMetadataFetcher metadataFetcher = mock(InstanceMetadataFetcher.class);
+
+ @BeforeEach
+ void setup()
+ {
+ vertx = Vertx.vertx();
+ executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
+ mockLifecycleProvider = mock(LifecycleProvider.class);
+ when(metadataFetcher.instance(TEST_HOST)).thenReturn(TEST_HOST_META);
+ when(metadataFetcher.instance(TEST_HOST_2)).thenReturn(TEST_HOST_2_META);
+ }
+
+ @AfterEach
+ void cleanup()
+ {
+ TestResourceReaper.create().with(vertx).with(executorPools).close();
+ }
+
+ @Test
+ void testGetLifecycleInfoWithNoTaskSubmitted()
+ {
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+
+ LifecycleInfoResponse response = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ assertThat(response.currentState()).isEqualTo(LifecycleCassandraState.STOPPED);
+ assertThat(response.desiredState()).isEqualTo(LifecycleCassandraState.UNKNOWN);
+ assertThat(response.status()).isEqualTo(LifecycleStatus.UNDEFINED);
+ assertThat(response.lastUpdate()).isEqualTo("No lifecycle task submitted for this instance yet.");
+ }
+
+ @Test
+ void testSubmittedTaskSucceeds() throws LifecycleTaskConflictException, InterruptedException
+ {
+ // Submit slow start task
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+ CountDownLatch startLatch = slowCassandraStart();
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+
+ LifecycleInfoResponse actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.STOPPED, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGING,
+ "Submitting start task for instance");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Wait for the task to complete
+ startLatch.countDown();
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(true);
+ Thread.sleep(200);
+
+ // Check status was updated
+ actualResponse = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.RUNNING, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGED,
+ "Instance has started");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Attempt to start the instance again, should be no-op since instance is already running
+ actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+ }
+
+ @Test
+ void testSubmittedTaskFails() throws LifecycleTaskConflictException, InterruptedException
+ {
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+
+ String errorMessage = "Cannot find Cassandra executable to start instance.";
+ doThrow(new RuntimeException(errorMessage)).when(mockLifecycleProvider).start(TEST_HOST_META);
+
+ lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ Thread.sleep(200);
Review Comment:
NIT about the usage of thread.sleep
##########
server/src/test/java/org/apache/cassandra/sidecar/lifecycle/LifecycleManagerTest.java:
##########
@@ -0,0 +1,300 @@
+/*
+ * 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.lifecycle;
+
+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 io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link LifecycleManager} to validate lifecycle management behavior
+ */
+class LifecycleManagerTest
+{
+ private static final String TEST_HOST = "127.0.0.1";
+ private static final InstanceMetadata TEST_HOST_META = mock(InstanceMetadata.class);
+ private static final String TEST_HOST_2 = "127.0.0.2";
+ private static final InstanceMetadata TEST_HOST_2_META = mock(InstanceMetadata.class);
+
+ protected Vertx vertx;
+ protected ExecutorPools executorPools;
+ protected LifecycleProvider mockLifecycleProvider;
+ protected InstanceMetadataFetcher metadataFetcher = mock(InstanceMetadataFetcher.class);
+
+ @BeforeEach
+ void setup()
+ {
+ vertx = Vertx.vertx();
+ executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
+ mockLifecycleProvider = mock(LifecycleProvider.class);
+ when(metadataFetcher.instance(TEST_HOST)).thenReturn(TEST_HOST_META);
+ when(metadataFetcher.instance(TEST_HOST_2)).thenReturn(TEST_HOST_2_META);
+ }
+
+ @AfterEach
+ void cleanup()
+ {
+ TestResourceReaper.create().with(vertx).with(executorPools).close();
+ }
+
+ @Test
+ void testGetLifecycleInfoWithNoTaskSubmitted()
+ {
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+
+ LifecycleInfoResponse response = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ assertThat(response.currentState()).isEqualTo(LifecycleCassandraState.STOPPED);
+ assertThat(response.desiredState()).isEqualTo(LifecycleCassandraState.UNKNOWN);
+ assertThat(response.status()).isEqualTo(LifecycleStatus.UNDEFINED);
+ assertThat(response.lastUpdate()).isEqualTo("No lifecycle task submitted for this instance yet.");
+ }
+
+ @Test
+ void testSubmittedTaskSucceeds() throws LifecycleTaskConflictException, InterruptedException
+ {
+ // Submit slow start task
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+ CountDownLatch startLatch = slowCassandraStart();
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+
+ LifecycleInfoResponse actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.STOPPED, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGING,
+ "Submitting start task for instance");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Wait for the task to complete
+ startLatch.countDown();
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(true);
+ Thread.sleep(200);
+
+ // Check status was updated
+ actualResponse = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.RUNNING, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGED,
+ "Instance has started");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Attempt to start the instance again, should be no-op since instance is already running
+ actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+ }
+
+ @Test
+ void testSubmittedTaskFails() throws LifecycleTaskConflictException, InterruptedException
+ {
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+
+ String errorMessage = "Cannot find Cassandra executable to start instance.";
+ doThrow(new RuntimeException(errorMessage)).when(mockLifecycleProvider).start(TEST_HOST_META);
+
+ lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ Thread.sleep(200);
+
+ LifecycleInfoResponse response = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ assertThat(response.currentState()).isEqualTo(LifecycleCassandraState.STOPPED);
+ assertThat(response.desiredState()).isEqualTo(LifecycleCassandraState.RUNNING);
+ assertThat(response.status()).isEqualTo(LifecycleStatus.DIVERGED);
+ assertThat(response.lastUpdate()).isEqualTo(String.format("Failed to start instance 127.0.0.1: %s", errorMessage));
+
+ lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ reset(mockLifecycleProvider);
+
+ Thread.sleep(200);
Review Comment:
NIT about the usage of thread.sleep
##########
integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import org.apache.cassandra.sidecar.testing.SharedClusterIntegrationTestBase;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Utility class to test different lifecycle provider implementations.
+ */
+public class LifecycleProviderIntegrationTester
+{
+ protected final Logger logger = LoggerFactory.getLogger(SharedClusterIntegrationTestBase.class);
Review Comment:
Do we want the `SharedClusterIntegrationTestBase` logger here?
```suggestion
protected final Logger logger = LoggerFactory.getLogger(LifecycleProviderIntegrationTester.class);
```
##########
server/src/test/java/org/apache/cassandra/sidecar/lifecycle/LifecycleManagerTest.java:
##########
@@ -0,0 +1,300 @@
+/*
+ * 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.lifecycle;
+
+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 io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link LifecycleManager} to validate lifecycle management behavior
+ */
+class LifecycleManagerTest
+{
+ private static final String TEST_HOST = "127.0.0.1";
+ private static final InstanceMetadata TEST_HOST_META = mock(InstanceMetadata.class);
+ private static final String TEST_HOST_2 = "127.0.0.2";
+ private static final InstanceMetadata TEST_HOST_2_META = mock(InstanceMetadata.class);
+
+ protected Vertx vertx;
+ protected ExecutorPools executorPools;
+ protected LifecycleProvider mockLifecycleProvider;
+ protected InstanceMetadataFetcher metadataFetcher = mock(InstanceMetadataFetcher.class);
+
+ @BeforeEach
+ void setup()
+ {
+ vertx = Vertx.vertx();
+ executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
+ mockLifecycleProvider = mock(LifecycleProvider.class);
+ when(metadataFetcher.instance(TEST_HOST)).thenReturn(TEST_HOST_META);
+ when(metadataFetcher.instance(TEST_HOST_2)).thenReturn(TEST_HOST_2_META);
+ }
+
+ @AfterEach
+ void cleanup()
+ {
+ TestResourceReaper.create().with(vertx).with(executorPools).close();
+ }
+
+ @Test
+ void testGetLifecycleInfoWithNoTaskSubmitted()
+ {
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+
+ LifecycleInfoResponse response = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ assertThat(response.currentState()).isEqualTo(LifecycleCassandraState.STOPPED);
+ assertThat(response.desiredState()).isEqualTo(LifecycleCassandraState.UNKNOWN);
+ assertThat(response.status()).isEqualTo(LifecycleStatus.UNDEFINED);
+ assertThat(response.lastUpdate()).isEqualTo("No lifecycle task submitted for this instance yet.");
+ }
+
+ @Test
+ void testSubmittedTaskSucceeds() throws LifecycleTaskConflictException, InterruptedException
+ {
+ // Submit slow start task
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(false);
+ CountDownLatch startLatch = slowCassandraStart();
+ LifecycleManager lifecycleManager = new LifecycleManager(metadataFetcher, mockLifecycleProvider, executorPools);
+
+ LifecycleInfoResponse actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.STOPPED, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGING,
+ "Submitting start task for instance");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Wait for the task to complete
+ startLatch.countDown();
+ when(mockLifecycleProvider.isRunning(TEST_HOST_META)).thenReturn(true);
+ Thread.sleep(200);
+
+ // Check status was updated
+ actualResponse = lifecycleManager.getLifecycleInfo(TEST_HOST);
+ expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.RUNNING, LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGED,
+ "Instance has started");
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+
+ // Attempt to start the instance again, should be no-op since instance is already running
+ actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
+ assertThat(actualResponse).isEqualTo(expectedResponse);
Review Comment:
We like to avoid doing thread sleep and we actually have a helper method that allows us to wait in a loop
```suggestion
// Check status was updated
LifecycleInfoResponse actualResponseAfterStart = lifecycleManager.getLifecycleInfo(TEST_HOST);
LifecycleInfoResponse expectedResponseAfterStart = new LifecycleInfoResponse(LifecycleCassandraState.RUNNING, LifecycleCassandraState.RUNNING,
LifecycleStatus.CONVERGED,
"Instance has started");
loopAssert(1, 10, () -> assertThat(actualResponseAfterStart).isEqualTo(expectedResponseAfterStart));
// Attempt to start the instance again, should be no-op since instance is already running
actualResponse = lifecycleManager.updateDesiredState(TEST_HOST, LifecycleCassandraState.RUNNING);
assertThat(actualResponse).isEqualTo(expectedResponseAfterStart);
```
##########
integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import org.apache.cassandra.sidecar.testing.SharedClusterIntegrationTestBase;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Utility class to test different lifecycle provider implementations.
+ */
+public class LifecycleProviderIntegrationTester
+{
+ protected final Logger logger = LoggerFactory.getLogger(SharedClusterIntegrationTestBase.class);
+
+ static final int TIMEOUT_SECONDS = 120;
+
+ final WebClient client;
+ final String sidecarHost;
+ final int sidecarPort;
+ final Runnable cassandraNodeCrasher;
+
+ public LifecycleProviderIntegrationTester(WebClient client, String sidecarHost, int sidecarPort, Runnable cassandraNodeCrasher)
+ {
+ this.client = client;
+ this.sidecarHost = sidecarHost;
+ this.sidecarPort = sidecarPort;
+ this.cassandraNodeCrasher = cassandraNodeCrasher;
+ }
+
+ void testLifecycleProviderStartAndStopAndRecoveryAfterCrash() throws Exception
+ {
+ // Check CQL health is NOT_OK, since node is not started
+ assertThat(getCqlStatus()).isEqualTo("NOT_OK");
+
+ // GET /lifecycle when node has not started and no intent has been submitted
+ HttpResponse<Buffer> lifecycleInfoOnStartup = getLifecycle();
+ assertExpectedLifecycleResponse(lifecycleInfoOnStartup, OK.code(),
+ "STOPPED", "UNKNOWN", "UNDEFINED",
+ "No lifecycle task submitted for this instance yet.");
+
+ // Start node via PUT /lifecycle and wait for node to be up
+ HttpResponse<Buffer> lifecycleUpdateStart = putLifecycle("start");
+ assertExpectedLifecycleResponse(lifecycleUpdateStart, ACCEPTED.code(),
+ "STOPPED", "RUNNING", "CONVERGING", "Submitting start task for instance");
+
+ waitForLastUpdateToConverge("Instance has started", TIMEOUT_SECONDS);
+ waitForCqlStatus("OK", TIMEOUT_SECONDS);
+
+ // GET /lifecycle after node has started up
+ HttpResponse<Buffer> lifecycleInfoAfterStartup = getLifecycle();
+ assertExpectedLifecycleResponse(lifecycleInfoAfterStartup, OK.code(),
+ "RUNNING", "RUNNING", "CONVERGED", "Instance has started");
+
+ // Try starting node with PUT /lifecycle again
+ HttpResponse<Buffer> lifecycleInfoAfterStartAgain = putLifecycle("start");
+ assertExpectedLifecycleResponse(lifecycleInfoAfterStartAgain, OK.code(),
+ "RUNNING", "RUNNING", "CONVERGED", "Instance has started");
+
+ // Simulate node crashing by directly calling the lifecycle provider's stop method
+ cassandraNodeCrasher.run();
+ waitForLastUpdateToConverge(String.format("Instance %s has unexpectedly diverged from the desired state RUNNING to STOPPED.", sidecarHost), TIMEOUT_SECONDS);
+
+ // CQL status should be down since instance is crashed
+ waitForCqlStatus("NOT_OK", TIMEOUT_SECONDS);
+
+ // GET /lifecycle after node has crashed
+ HttpResponse<Buffer> lifecycleInfoAfterCrash = getLifecycle();
+ assertExpectedLifecycleResponse(lifecycleInfoAfterCrash, OK.code(),
+ "STOPPED", "RUNNING", "DIVERGED",
+ String.format("Instance %s has unexpectedly diverged from the desired state RUNNING to STOPPED.", sidecarHost));
+
+ // Try starting node with PUT /lifecycle again
+ HttpResponse<Buffer> lifecycleInfoOnStartAfterCrash = putLifecycle("start");
+ assertExpectedLifecycleResponse(lifecycleInfoOnStartAfterCrash, ACCEPTED.code(),
+ "STOPPED", "RUNNING", "CONVERGING", "Submitting start task for instance");
+ waitForLastUpdateToConverge("Instance has started", TIMEOUT_SECONDS);
+ waitForCqlStatus("OK", TIMEOUT_SECONDS);
+
+ // Confirm that the node is up again with GET /lifecycle
+ HttpResponse<Buffer> lifecycleInfoAfterRecovery = getLifecycle();
+ assertExpectedLifecycleResponse(lifecycleInfoAfterRecovery, OK.code(),
+ "RUNNING", "RUNNING", "CONVERGED", "Instance has started");
+
+ // Stop node via PUT /lifecycle
+ HttpResponse<Buffer> lifecycleUpdateStop = putLifecycle("stop");
+ assertExpectedLifecycleResponse(lifecycleUpdateStop, ACCEPTED.code(),
+ "RUNNING", "STOPPED", "CONVERGING", "Submitting stop task for instance");
+
+ // Allow time for the async stop task to complete
+ waitForLastUpdateToConverge("Instance has stopped", TIMEOUT_SECONDS);
+
+ // GET /lifecycle after node has stopped
+ HttpResponse<Buffer> lifecycleInfoAfterStop = getLifecycle();
+ assertExpectedLifecycleResponse(lifecycleInfoAfterStop, OK.code(),
+ "STOPPED", "STOPPED", "CONVERGED", "Instance has stopped");
+
+ // CQL status should be down since instance is stopped
+ assertThat(getCqlStatus()).isEqualTo("NOT_OK");
+
+ // Try stopping node with PUT /lifecycle again, should be idempotent
+ HttpResponse<Buffer> lifecycleInfoOnStopAgain = putLifecycle("stop");
+ assertExpectedLifecycleResponse(lifecycleInfoOnStopAgain, OK.code(),
+ "STOPPED", "STOPPED", "CONVERGED", "Instance has stopped");
+ }
+
+ private void assertExpectedLifecycleResponse(
+ HttpResponse<Buffer> response,
+ int expectedStatusCode,
+ String currentState,
+ String desiredState,
+ String status,
+ String lastUpdate)
+ {
+ assertThat(response.statusCode()).isEqualTo(expectedStatusCode);
+ JsonObject body = response.bodyAsJsonObject();
+ assertThat(body.getString("current_state")).isEqualTo(currentState);
+ assertThat(body.getString("desired_state")).isEqualTo(desiredState);
+ assertThat(body.getString("status")).isEqualTo(status);
+ assertThat(body.getString("last_update")).isEqualTo(lastUpdate);
+ }
+
+ private HttpResponse<Buffer> getLifecycle()
+ {
+ return getBlocking(client
+ .get(sidecarPort, sidecarHost, "/api/v1/cassandra/lifecycle")
+ .send());
+ }
+
+ private HttpResponse<Buffer> putLifecycle(String desiredState)
+ {
+ return getBlocking(client
+ .put(sidecarPort, sidecarHost, "/api/v1/cassandra/lifecycle")
+ .sendBuffer(Buffer.buffer("{\"state\":\"" + desiredState + "\"}")));
Review Comment:
NIT: this is a little bit easier to read and a bit simpler to build using `io.vertx.core.json.JsonObject`
```suggestion
.sendBuffer(JsonObject.of("state", desiredState).toBuffer()));
```
##########
server/src/test/java/org/apache/cassandra/sidecar/handlers/LifecycleUpdateHandlerTest.java:
##########
@@ -0,0 +1,205 @@
+/*
+ * 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.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 com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+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.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleManager;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.CONFLICT;
+import static io.vertx.core.buffer.Buffer.buffer;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link LifecycleUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class LifecycleUpdateHandlerTest
+{
+ Vertx vertx;
+ Server server;
+ LifecycleManager mockLifecycleManager = mock(LifecycleManager.class);
+
+ @BeforeEach
+ void before() throws InterruptedException
+ {
+ Injector injector;
+ Module testOverride = Modules.override(new TestModule()).with(new LifecycleUpdateHandlerTestModule());
+ injector = Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+ vertx = injector.getInstance(Vertx.class);
+ server = injector.getInstance(Server.class);
+ VertxTestContext context = new VertxTestContext();
+ server.start().onSuccess(s -> context.completeNow()).onFailure(context::failNow);
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ reset(mockLifecycleManager);
+ }
+
+ @AfterEach
+ void after() throws InterruptedException
+ {
+ getBlocking(TestResourceReaper.create().with(server).close(), 60, TimeUnit.SECONDS, "Closing server");
+ }
+
+ @Test
+ void testSuccessfulPutWithAcceptedResponse(VertxTestContext ctx) throws LifecycleTaskConflictException
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"start\"}";
Review Comment:
alternatively can use JsonObject
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#issuecomment-3312548537
CI: https://app.circleci.com/pipelines/github/andresbeckruiz/cassandra-sidecar/4/workflows/0759ceb8-dc20-48e7-8922-3037037129db
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "yifan-c (via GitHub)" <gi...@apache.org>.
yifan-c commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2380482950
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleCassandraState.java:
##########
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+import static org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload.State;
+
+/**
+ * Represents the lifecycle state of a Cassandra instance.
+ */
+public enum LifecycleCassandraState
Review Comment:
How about `CassandraLifecycleState`? "CassandraLifecycle" modifies "State", indicating what kind of state it is.
I noticed that you also have `LifecycleStatus`. Maybe your idea is to have the `Lifecycle` scope, and have `CassandraState` in the scope. In this case, maybe have something like this, which better indicates the scope, e.g. `Lifecycle.CassandraState`
```java
class Lifecycle
{
enum CassandraState { .. }
enum OperationStatus { .. }
}
```
##########
server/src/main/java/org/apache/cassandra/sidecar/handlers/LifecycleUpdateHandler.java:
##########
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.inject.Singleton;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.http.HttpServerRequest;
+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 io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleManager;
+import org.apache.cassandra.sidecar.utils.HttpExceptions;
+
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.common.data.LifecycleCassandraState.fromNodeCommandState;
+
+/**
+ * Handles {@code PUT /api/v1/cassandra/lifecycle} requests to start or stop a Cassandra node.
+ *
+ * <p> Expects a JSON payload:
+ * { "state": "start" } or { "state": "stop" }
+ * and will record the desired state. </p>
+ */
+@Singleton
+public class LifecycleUpdateHandler extends NodeCommandHandler implements AccessProtected
+{
+ private final LifecycleManager lifecycleManager;
+
+ @Inject
+ public LifecycleUpdateHandler(InstanceMetadataFetcher metadataFetcher, ExecutorPools executorPools, LifecycleManager lifecycleManager)
+ {
+ super(metadataFetcher, executorPools, null);
+ this.lifecycleManager = lifecycleManager;
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return Collections.singleton(BasicPermissions.MODIFY_LIFECYCLE.toAuthorization());
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext context,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ NodeCommandRequestPayload request)
+ {
+ LifecycleCassandraState desiredState = fromNodeCommandState(request.state());
+ executorPools.service()
+ .executeBlocking(() -> lifecycleManager.updateDesiredState(host, desiredState))
+ .onSuccess(info ->
+ {
+ switch (info.status())
+ {
+ case CONVERGED:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.OK.code())
+ .end(Json.encode(info));
+ break;
+ case CONVERGING:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.ACCEPTED.code())
+ .end(Json.encode(info));
+ break;
+ default:
+ logger.warn("{} request failed with unexpected result. request={}, remoteAddress={}, instance={}, lifecycleStatus={}",
+ this.getClass().getSimpleName(), request, remoteAddress, host, info.status());
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
+ .end(Json.encode(info));
+ }
Review Comment:
nit: it could be simplified.
```suggestion
HttpServerResponse response = context.response().putHeader("Content-Type", "application/json");
switch (info.status())
{
case CONVERGED:
response.setStatusCode(HttpResponseStatus.OK.code());
break;
case CONVERGING:
response.setStatusCode(HttpResponseStatus.ACCEPTED.code());
break;
default:
logger.warn("{} request failed with unexpected result. request={}, remoteAddress={}, instance={}, lifecycleStatus={}",
this.getClass().getSimpleName(), request, remoteAddress, host, info.status());
response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
}
response.end(Json.encode(info));
```
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleStatus.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+/**
+ * Represents the lifecycle status of this instance relative to the desired state.
+ */
+public enum LifecycleStatus
+{
+ /**
+ * The status when a desired lifecycle state has not been submitted yet
+ */
+ UNDEFINED,
+
+ /**
+ * The status when the current lifecycle state of an instance matches the desired lifecycle state
+ */
+ CONVERGED,
+
+ /**
+ * The status when the current lifecycle state of an instance does not match the desired lifecycle state
+ */
+ DIVERGED,
+
+ /**
+ * The status when an instance is starting or stopping to match the desired lifecycle state
+ */
+ CONVERGING;
Review Comment:
How to distinguish `DIVERGED` and `CONVERGING`? They both means the target state does not match the current state.
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleStatus.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+/**
+ * Represents the lifecycle status of this instance relative to the desired state.
+ */
+public enum LifecycleStatus
Review Comment:
How about `LifecycleOperationStatus`?
(Although I feel that `State` and `Status` might be confusing.)
##########
integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/InJvmLifecycleProviderIntegrationTest.java:
##########
@@ -0,0 +1,86 @@
+/*
+ * 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.lifecycle;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
+import org.apache.cassandra.testing.ClusterBuilderConfiguration;
+
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+/**
+ * Tests the {@link InJvmDTestLifecycleProvider} to ensure it can start and stop a Cassandra node
+ */
+public class InJvmLifecycleProviderIntegrationTest extends SharedClusterSidecarIntegrationTestBase
+{
+ static final InstanceMetadata LOCALHOST_METADATA = mock(InstanceMetadata.class);
+ private static final String JVM_LIFECYCLE_TEST_MIN_VERSION = "4.1";
+
+ @BeforeAll
+ static void beforeAll()
+ {
+ when(LOCALHOST_METADATA.host()).thenReturn("localhost");
+ }
+
+ @Override
+ protected ClusterBuilderConfiguration testClusterConfiguration()
+ {
+ return super.testClusterConfiguration().startCluster(false);
+ }
+
+ @Override
+ protected void beforeClusterProvisioning()
+ {
+ // JVM Distributed Test framework contains a bug with restarting nodes in version 4.0 (CASSANDRA-19729)
+ assumeThat(SimpleCassandraVersion.create(testVersion.version()))
+ .withFailMessage("JVM Distributed Test framework contains a bug with restarting nodes in version 4.0 (CASSANDRA-19729)")
Review Comment:
I do not think you want to override the error message completely, and probably want to use `as(..)` instead.
Please check out the difference between `as(..)` and `withFailMessage(..)`.
##########
server/src/test/java/org/apache/cassandra/sidecar/handlers/LifecycleUpdateHandlerTest.java:
##########
@@ -0,0 +1,205 @@
+/*
+ * 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.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 com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleManager;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.CONFLICT;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link LifecycleUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class LifecycleUpdateHandlerTest
+{
+ Vertx vertx;
+ Server server;
+ LifecycleManager mockLifecycleManager = mock(LifecycleManager.class);
+
+ @BeforeEach
+ void before() throws InterruptedException
+ {
+ Injector injector;
+ Module testOverride = Modules.override(new TestModule()).with(new LifecycleUpdateHandlerTestModule());
+ injector = Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+ vertx = injector.getInstance(Vertx.class);
+ server = injector.getInstance(Server.class);
+ VertxTestContext context = new VertxTestContext();
+ server.start().onSuccess(s -> context.completeNow()).onFailure(context::failNow);
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ reset(mockLifecycleManager);
+ }
+
+ @AfterEach
+ void after() throws InterruptedException
+ {
+ getBlocking(TestResourceReaper.create().with(server).close(), 60, TimeUnit.SECONDS, "Closing server");
+ }
+
+ @Test
+ void testSuccessfulPutWithAcceptedResponse(VertxTestContext ctx) throws LifecycleTaskConflictException
+ {
+ WebClient client = WebClient.create(vertx);
+ JsonObject payload = JsonObject.of("state", "start");
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.STOPPED,
+ LifecycleCassandraState.RUNNING,
+ LifecycleStatus.CONVERGING,
+ "Submitted task to start instance");
+ when(mockLifecycleManager.updateDesiredState("127.0.0.1", LifecycleCassandraState.RUNNING))
+ .thenReturn(expectedResponse);
+ client.put(server.actualPort(), "127.0.0.1", "/api/v1/cassandra/lifecycle")
+ .sendBuffer(payload.toBuffer(), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockLifecycleManager, times(1)).updateDesiredState("127.0.0.1", LifecycleCassandraState.RUNNING);
+ assertThat(resp.bodyAsJson(LifecycleInfoResponse.class)).isEqualTo(expectedResponse);
+ assertThat(resp.statusCode()).isEqualTo(ACCEPTED.code());
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testSuccessfulPutWithOKResponse(VertxTestContext ctx) throws LifecycleTaskConflictException
+ {
+ WebClient client = WebClient.create(vertx);
+ JsonObject payload = JsonObject.of("state", "stop");
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.STOPPED,
+ LifecycleCassandraState.STOPPED,
+ LifecycleStatus.CONVERGED,
+ "Submitted task to stop instance");
+ when(mockLifecycleManager.updateDesiredState("127.0.0.1", LifecycleCassandraState.STOPPED)).thenReturn(expectedResponse);
+ client.put(server.actualPort(), "127.0.0.1", "/api/v1/cassandra/lifecycle")
+ .sendBuffer(payload.toBuffer(), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockLifecycleManager, times(1)).updateDesiredState("127.0.0.1", LifecycleCassandraState.STOPPED);
+ assertThat(resp.bodyAsJson(LifecycleInfoResponse.class)).isEqualTo(expectedResponse);
+ assertThat(resp.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testSuccessfulPutWithFailedResponse(VertxTestContext ctx) throws LifecycleTaskConflictException
+ {
+ WebClient client = WebClient.create(vertx);
+ JsonObject payload = JsonObject.of("state", "stop");
+ LifecycleInfoResponse expectedResponse = new LifecycleInfoResponse(LifecycleCassandraState.RUNNING,
+ LifecycleCassandraState.STOPPED,
+ LifecycleStatus.DIVERGED,
+ "Error while stopping instance");
+ when(mockLifecycleManager.updateDesiredState("127.0.0.1", LifecycleCassandraState.STOPPED)).thenReturn(expectedResponse);
+ client.put(server.actualPort(), "127.0.0.1", "/api/v1/cassandra/lifecycle")
+ .sendBuffer(payload.toBuffer(), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockLifecycleManager, times(1)).updateDesiredState("127.0.0.1", LifecycleCassandraState.STOPPED);
+ assertThat(resp.bodyAsJson(LifecycleInfoResponse.class)).isEqualTo(expectedResponse);
+ assertThat(resp.statusCode()).isEqualTo(HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testInvalidState(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ JsonObject payload = JsonObject.of("state", "invalid");
+ client.put(server.actualPort(), "127.0.0.1", "/api/v1/cassandra/lifecycle")
+ .sendBuffer(payload.toBuffer(), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ assertThat(resp.statusCode()).isEqualTo(BAD_REQUEST.code());
+ verify(mockLifecycleManager, times(0)).updateDesiredState("127.0.0.1", LifecycleCassandraState.RUNNING);
+ verify(mockLifecycleManager, times(0)).updateDesiredState("127.0.0.1", LifecycleCassandraState.STOPPED);
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testSubmitTaskAlreadyInProgress(VertxTestContext ctx) throws LifecycleTaskConflictException
+ {
+ // Setup mock to throw conflict exception
+ doThrow(new LifecycleTaskConflictException("Cannot start host 127.0.0.1. Task already in progress for this host."))
+ .when(mockLifecycleManager).updateDesiredState("127.0.0.1", LifecycleCassandraState.RUNNING);
+
+ WebClient client = WebClient.create(vertx);
+ JsonObject payload = JsonObject.of("state", "start");
+ client.put(server.actualPort(), "127.0.0.1", "/api/v1/cassandra/lifecycle")
+ .sendBuffer(payload.toBuffer(), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ assertThat(resp.statusCode()).isEqualTo(CONFLICT.code());
+ verify(mockLifecycleManager, times(1)).updateDesiredState("127.0.0.1", LifecycleCassandraState.RUNNING);
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ /**
+ * Test guice module for {@link LifecycleUpdateHandler} tests
+ */
+ class LifecycleUpdateHandlerTestModule extends AbstractModule
+ {
+
+ @Provides
+ @Singleton
+ public LifecycleManager intentManager()
Review Comment:
nit: should the method be named `lifecycleManager()`?
##########
server/src/main/java/org/apache/cassandra/sidecar/modules/SidecarModules.java:
##########
@@ -61,7 +61,8 @@ public static List<Module> all(@Nullable Path confPath)
new UtilitiesModule(),
new MultiBindingTypeResolverModule(),
new LiveMigrationModule(),
- new OpenApiModule());
+ new OpenApiModule(),
+ new LifecycleModule());
Review Comment:
nit: the list of modules used to alphabetically ordered.
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "frankgh (via GitHub)" <gi...@apache.org>.
frankgh commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2340404928
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/response/LifecycleInfoResponse.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.response;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+
+/**
+ * A class representing a response for the {@code LifecycleInfoRequest}.
+ */
+public class LifecycleInfoResponse
+{
+
+ private final LifecycleCassandraState currentState;
+ private final LifecycleCassandraState desiredState;
+ private final LifecycleStatus status;
+ private final String lastUpdate;
+
+ /**
+ * Constructs a {@link LifecycleInfoResponse} object with the {@code currentState}, {@code intendedState}, {@code result},
+ * and {@code message}
+ *
+ * @param currentState the current state of the Cassandra node
+ * @param desiredState the intended state of the Cassandra node
+ * @param status the result of the last lifecycle operation
+ * @param lastUpdate a message providing additional context about the last lifecycle operation
+ */
+ @JsonCreator
+ public LifecycleInfoResponse(@JsonProperty("current_state") LifecycleCassandraState currentState,
+ @JsonProperty("desired_state") LifecycleCassandraState desiredState,
+ @JsonProperty("status") LifecycleStatus status,
+ @JsonProperty("last_update") String lastUpdate)
+ {
+ this.currentState = Objects.requireNonNull(currentState, "State must be non-null");
+ this.desiredState = desiredState;
+ this.status = status;
+ this.lastUpdate = lastUpdate;
+ }
+
+ /**
+ * @return the current state of the Cassandra node
+ */
+ @JsonProperty("current_state")
+ public LifecycleCassandraState currentState()
+ {
+ return currentState;
+ }
+
+ /**
+ * @return the intended state of the Cassandra node
+ */
+ @JsonProperty("desired_state")
+ public LifecycleCassandraState desiredState()
+ {
+ return desiredState;
+ }
+
+ /**
+ * @return the status of the last lifecycle state
+ */
+ @JsonProperty("status")
+ public LifecycleStatus status()
+ {
+ return status;
+ }
+
+ /**
+ * @return message providing additional context about the last lifecycle operation
+ */
+ @JsonProperty("last_update")
+ public String lastUpdate()
+ {
+ return lastUpdate;
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (!(o instanceof LifecycleInfoResponse)) return false;
+ LifecycleInfoResponse that = (LifecycleInfoResponse) o;
+ return currentState == that.currentState && desiredState == that.desiredState && status == that.status && Objects.equals(lastUpdate, that.lastUpdate);
+ }
+
+ public int hashCode()
+ {
+ return Objects.hash(currentState, desiredState, status, lastUpdate);
+ }
+
+ public String toString()
Review Comment:
NIT
```suggestion
@Override
public String toString()
```
##########
server/src/main/java/org/apache/cassandra/sidecar/modules/LifecycleModule.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.ProvidesIntoMap;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.handlers.LifecycleInfoHandler;
+import org.apache.cassandra.sidecar.handlers.LifecycleUpdateHandler;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleProvider;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+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.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides the telemetry capability
+ */
+public class LifecycleModule extends AbstractModule
+{
+ @Provides
+ @Singleton
+ LifecycleProvider lifecycleProvider(SidecarConfiguration sidecarConfiguration)
+ {
+ LifecycleConfiguration lifecycleConfiguration = sidecarConfiguration.lifecycleConfiguration();
+ if (!lifecycleConfiguration.enabled())
+ {
+ return getNoopProvider();
+ }
+
+ ParameterizedClassConfiguration providerClass = lifecycleConfiguration.lifecycleProvider();
+ if (providerClass == null)
+ {
+ throw new ConfigurationException("Lifecycle management is enabled, but provider not set.");
+ }
+
+ if (providerClass.className().equalsIgnoreCase(ProcessLifecycleProvider.class.getName()))
+ {
+ Map<String, String> params = new HashMap<>();
+ Map<String, String> namedParams = providerClass.namedParameters();
+ if (namedParams != null)
+ {
+ params.putAll(namedParams);
+ }
+ return new ProcessLifecycleProvider(params);
+ }
+
+ throw new ConfigurationException("Unrecognized authorization provider " + providerClass.className() + " set");
+ }
+
+ private static @NotNull LifecycleProvider getNoopProvider()
+ {
+ return new LifecycleProvider()
+ {
+ public void start(InstanceMetadata instance)
Review Comment:
can we add missing override annotations here?
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleStatus.java:
##########
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+/**
+ * Represents the lifecycle status of this instance relative to the desired state.
+ */
+public enum LifecycleStatus
+{
+ UNDEFINED,
Review Comment:
can we add javadocs for all the enum values here ? something concise but that gives us an idea of when we have each state
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/ProcessLifecycleProvider.java:
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.lifecycle;
+
+import java.util.Map;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * A {@link LifecycleProvider} that manages Cassandra instances as OS processes.
+ * <p>
+ * This implementation is a placeholder and is not yet implemented.
+ */
+public class ProcessLifecycleProvider implements LifecycleProvider
+{
+ public ProcessLifecycleProvider(Map<String, String> params)
+ {
+ // Params unused for now
+ }
+
+ public void start(InstanceMetadata instance)
Review Comment:
```suggestion
@Override
public void start(InstanceMetadata instance)
```
##########
server/src/main/java/org/apache/cassandra/sidecar/handlers/LifecycleUpdateHandler.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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 com.google.inject.Singleton;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.http.HttpServerRequest;
+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 io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleManager;
+import org.apache.cassandra.sidecar.utils.HttpExceptions;
+
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.common.data.LifecycleCassandraState.fromNodeCommandState;
+
+/**
+ * Handles {@code PUT /api/v1/cassandra/lifecycle} requests to start or stop a Cassandra node.
+ *
+ * <p> Expects a JSON payload:
+ * { "state": "start" } or { "state": "stop" }
+ * and will record the desired state. </p>
+ */
+@Singleton
+public class LifecycleUpdateHandler extends NodeCommandHandler implements AccessProtected
+{
+ private final LifecycleManager lifecycleManager;
+
+ @Inject
+ public LifecycleUpdateHandler(InstanceMetadataFetcher metadataFetcher, ExecutorPools executorPools, LifecycleManager lifecycleManager)
+ {
+ super(metadataFetcher, executorPools, null);
+ this.lifecycleManager = lifecycleManager;
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return Collections.singleton(BasicPermissions.MODIFY_LIFECYCLE.toAuthorization());
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext context,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ NodeCommandRequestPayload request)
+ {
+ LifecycleCassandraState desiredState = fromNodeCommandState(request.state());
+ executorPools.service()
+ .executeBlocking(() -> lifecycleManager.updateDesiredState(host, desiredState))
+ .onSuccess(info ->
+ {
+ switch (info.status())
+ {
+ case CONVERGED:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.OK.code())
+ .end(Json.encode(info));
+ break;
+ case CONVERGING:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.ACCEPTED.code())
+ .end(Json.encode(info));
+ break;
+ default:
+ logger.warn("{} request failed with unexpected result. request={}, remoteAddress={}, instance={}",
Review Comment:
can we log the value of `info.status()` here as well. It would help determine whether we are diverging or if the status is undefined
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/ProcessLifecycleProvider.java:
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.lifecycle;
+
+import java.util.Map;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * A {@link LifecycleProvider} that manages Cassandra instances as OS processes.
+ * <p>
+ * This implementation is a placeholder and is not yet implemented.
+ */
+public class ProcessLifecycleProvider implements LifecycleProvider
+{
+ public ProcessLifecycleProvider(Map<String, String> params)
+ {
+ // Params unused for now
+ }
+
+ public void start(InstanceMetadata instance)
+ {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void stop(InstanceMetadata instance)
+ {
+ throw new UnsupportedOperationException("Not implemented yet");
+
+ }
+
+ public boolean isRunning(InstanceMetadata instance)
Review Comment:
```suggestion
@Override
public boolean isRunning(InstanceMetadata instance)
```
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleCassandraState.java:
##########
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+import static org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload.State;
+
+/**
+ * Represents the lifecycle state of a Cassandra instance.
+ */
+public enum LifecycleCassandraState
+{
+ UNKNOWN,
Review Comment:
can we add javadocs with a small description here?
##########
server/src/main/java/org/apache/cassandra/sidecar/config/LifecycleConfiguration.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.config;
+
+/**
+ * Configuration for Cassandra lifecycle management service
+ */
+public interface LifecycleConfiguration
+{
+
+ /**
+ * @return {@code true} if lifecycle management is enabled, {@code false} otherwise
+ */
+ Boolean enabled();
Review Comment:
use primitive here
```suggestion
boolean enabled();
```
##########
integration-framework/src/main/java/org/apache/cassandra/sidecar/lifecycle/InJvmDTestLifecycleProvider.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of JVM Dtest Casasndra instances.
+ * This should used for integration tests where Cassandra instances are started and stopped
+ */
+public class InJvmDTestLifecycleProvider implements LifecycleProvider
+{
+ private final Iterable<? extends IInstance> instances;
+
+ public InJvmDTestLifecycleProvider(Iterable<? extends IInstance> instances)
+ {
+ this.instances = instances;
+ }
+
+ public void start(InstanceMetadata instanceMetadata)
+ {
+ getInstance(instanceMetadata).startup();
+ }
+
+ public void stop(InstanceMetadata instanceMetadata)
Review Comment:
```suggestion
@Override
public void stop(InstanceMetadata instanceMetadata)
```
##########
server/src/main/java/org/apache/cassandra/sidecar/config/yaml/LifecycleConfigurationImpl.java:
##########
@@ -0,0 +1,70 @@
+/*
+ * 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.config.yaml;
+
+import java.util.Collections;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+
+/**
+ * Configuration for Cassandra lifecycle management service
+ */
+public class LifecycleConfigurationImpl implements LifecycleConfiguration
+{
+ private static final boolean DEFAULT_ENABLED = false;
+ private static final ParameterizedClassConfiguration DEFAULT_LIFECYCLE_PROVIDER =
+ new ParameterizedClassConfigurationImpl(ProcessLifecycleProvider.class.getName(), Collections.emptyMap());
+
+ protected final Boolean enabled;
+
+ protected final String directory;
Review Comment:
is this something we'll be using in the future?
##########
integration-framework/src/main/java/org/apache/cassandra/sidecar/lifecycle/InJvmDTestLifecycleProvider.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of JVM Dtest Casasndra instances.
Review Comment:
typo
```suggestion
* Manages the lifecycle of JVM Dtest Cassandra instances.
```
##########
server/src/main/java/org/apache/cassandra/sidecar/config/LifecycleConfiguration.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.config;
+
+/**
+ * Configuration for Cassandra lifecycle management service
+ */
+public interface LifecycleConfiguration
Review Comment:
we should add the configuration in `sidecar.yaml` and have user-facing documentation for the feature
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/LifecycleProvider.java:
##########
@@ -0,0 +1,34 @@
+/*
+ * 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.lifecycle;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of Cassandra instances through Sidecar
+ */
+public interface LifecycleProvider
+{
+ void start(InstanceMetadata instance);
Review Comment:
let's add javadocs for all methods in public facing interfaces
##########
integration-framework/src/main/java/org/apache/cassandra/sidecar/lifecycle/InJvmDTestLifecycleProvider.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of JVM Dtest Casasndra instances.
+ * This should used for integration tests where Cassandra instances are started and stopped
+ */
+public class InJvmDTestLifecycleProvider implements LifecycleProvider
+{
+ private final Iterable<? extends IInstance> instances;
+
+ public InJvmDTestLifecycleProvider(Iterable<? extends IInstance> instances)
+ {
+ this.instances = instances;
+ }
+
+ public void start(InstanceMetadata instanceMetadata)
Review Comment:
let's add override annotations in this class
```suggestion
@Override
public void start(InstanceMetadata instanceMetadata)
```
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/LifecycleTaskInfo.java:
##########
@@ -0,0 +1,58 @@
+/*
+ * 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.lifecycle;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a lifecycle task
+ */
+public class LifecycleTaskInfo
Review Comment:
this seems unused, will this be used in the future? and if so does it make sense to add it later when needed?
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/response/LifecycleInfoResponse.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.response;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+
+/**
+ * A class representing a response for the {@code LifecycleInfoRequest}.
+ */
+public class LifecycleInfoResponse
+{
+
+ private final LifecycleCassandraState currentState;
+ private final LifecycleCassandraState desiredState;
+ private final LifecycleStatus status;
+ private final String lastUpdate;
+
+ /**
+ * Constructs a {@link LifecycleInfoResponse} object with the {@code currentState}, {@code intendedState}, {@code result},
+ * and {@code message}
+ *
+ * @param currentState the current state of the Cassandra node
+ * @param desiredState the intended state of the Cassandra node
+ * @param status the result of the last lifecycle operation
+ * @param lastUpdate a message providing additional context about the last lifecycle operation
+ */
+ @JsonCreator
+ public LifecycleInfoResponse(@JsonProperty("current_state") LifecycleCassandraState currentState,
+ @JsonProperty("desired_state") LifecycleCassandraState desiredState,
+ @JsonProperty("status") LifecycleStatus status,
+ @JsonProperty("last_update") String lastUpdate)
+ {
+ this.currentState = Objects.requireNonNull(currentState, "State must be non-null");
+ this.desiredState = desiredState;
+ this.status = status;
+ this.lastUpdate = lastUpdate;
+ }
+
+ /**
+ * @return the current state of the Cassandra node
+ */
+ @JsonProperty("current_state")
+ public LifecycleCassandraState currentState()
+ {
+ return currentState;
+ }
+
+ /**
+ * @return the intended state of the Cassandra node
+ */
+ @JsonProperty("desired_state")
+ public LifecycleCassandraState desiredState()
+ {
+ return desiredState;
+ }
+
+ /**
+ * @return the status of the last lifecycle state
+ */
+ @JsonProperty("status")
+ public LifecycleStatus status()
+ {
+ return status;
+ }
+
+ /**
+ * @return message providing additional context about the last lifecycle operation
+ */
+ @JsonProperty("last_update")
+ public String lastUpdate()
+ {
+ return lastUpdate;
+ }
+
+ public boolean equals(Object o)
Review Comment:
NIT
```suggestion
@Override
public boolean equals(Object o)
```
##########
integration-framework/src/main/java/org/apache/cassandra/sidecar/lifecycle/InJvmDTestLifecycleProvider.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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.lifecycle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * Manages the lifecycle of JVM Dtest Casasndra instances.
+ * This should used for integration tests where Cassandra instances are started and stopped
+ */
+public class InJvmDTestLifecycleProvider implements LifecycleProvider
+{
+ private final Iterable<? extends IInstance> instances;
+
+ public InJvmDTestLifecycleProvider(Iterable<? extends IInstance> instances)
+ {
+ this.instances = instances;
+ }
+
+ public void start(InstanceMetadata instanceMetadata)
+ {
+ getInstance(instanceMetadata).startup();
+ }
+
+ public void stop(InstanceMetadata instanceMetadata)
+ {
+ try
+ {
+ // Synchronous to ensure JMX port is released before start again
+ getInstance(instanceMetadata).shutdown().get(1, TimeUnit.MINUTES);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isRunning(InstanceMetadata host)
Review Comment:
```suggestion
@Override
public boolean isRunning(InstanceMetadata host)
```
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/response/LifecycleInfoResponse.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.response;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+
+/**
+ * A class representing a response for the {@code LifecycleInfoRequest}.
+ */
+public class LifecycleInfoResponse
+{
+
+ private final LifecycleCassandraState currentState;
+ private final LifecycleCassandraState desiredState;
+ private final LifecycleStatus status;
+ private final String lastUpdate;
+
+ /**
+ * Constructs a {@link LifecycleInfoResponse} object with the {@code currentState}, {@code intendedState}, {@code result},
+ * and {@code message}
+ *
+ * @param currentState the current state of the Cassandra node
+ * @param desiredState the intended state of the Cassandra node
+ * @param status the result of the last lifecycle operation
+ * @param lastUpdate a message providing additional context about the last lifecycle operation
+ */
+ @JsonCreator
+ public LifecycleInfoResponse(@JsonProperty("current_state") LifecycleCassandraState currentState,
+ @JsonProperty("desired_state") LifecycleCassandraState desiredState,
+ @JsonProperty("status") LifecycleStatus status,
+ @JsonProperty("last_update") String lastUpdate)
+ {
+ this.currentState = Objects.requireNonNull(currentState, "State must be non-null");
+ this.desiredState = desiredState;
+ this.status = status;
+ this.lastUpdate = lastUpdate;
+ }
+
+ /**
+ * @return the current state of the Cassandra node
+ */
+ @JsonProperty("current_state")
+ public LifecycleCassandraState currentState()
+ {
+ return currentState;
+ }
+
+ /**
+ * @return the intended state of the Cassandra node
+ */
+ @JsonProperty("desired_state")
+ public LifecycleCassandraState desiredState()
+ {
+ return desiredState;
+ }
+
+ /**
+ * @return the status of the last lifecycle state
+ */
+ @JsonProperty("status")
+ public LifecycleStatus status()
+ {
+ return status;
+ }
+
+ /**
+ * @return message providing additional context about the last lifecycle operation
+ */
+ @JsonProperty("last_update")
+ public String lastUpdate()
+ {
+ return lastUpdate;
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (!(o instanceof LifecycleInfoResponse)) return false;
+ LifecycleInfoResponse that = (LifecycleInfoResponse) o;
+ return currentState == that.currentState && desiredState == that.desiredState && status == that.status && Objects.equals(lastUpdate, that.lastUpdate);
Review Comment:
NIT for better readability
```suggestion
return currentState == that.currentState
&& desiredState == that.desiredState
&& status == that.status
&& Objects.equals(lastUpdate, that.lastUpdate);
```
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/response/LifecycleInfoResponse.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.response;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.data.LifecycleStatus;
+
+/**
+ * A class representing a response for the {@code LifecycleInfoRequest}.
+ */
+public class LifecycleInfoResponse
+{
+
+ private final LifecycleCassandraState currentState;
+ private final LifecycleCassandraState desiredState;
+ private final LifecycleStatus status;
+ private final String lastUpdate;
+
+ /**
+ * Constructs a {@link LifecycleInfoResponse} object with the {@code currentState}, {@code intendedState}, {@code result},
+ * and {@code message}
+ *
+ * @param currentState the current state of the Cassandra node
+ * @param desiredState the intended state of the Cassandra node
+ * @param status the result of the last lifecycle operation
+ * @param lastUpdate a message providing additional context about the last lifecycle operation
+ */
+ @JsonCreator
+ public LifecycleInfoResponse(@JsonProperty("current_state") LifecycleCassandraState currentState,
+ @JsonProperty("desired_state") LifecycleCassandraState desiredState,
+ @JsonProperty("status") LifecycleStatus status,
+ @JsonProperty("last_update") String lastUpdate)
+ {
+ this.currentState = Objects.requireNonNull(currentState, "State must be non-null");
+ this.desiredState = desiredState;
+ this.status = status;
+ this.lastUpdate = lastUpdate;
+ }
+
+ /**
+ * @return the current state of the Cassandra node
+ */
+ @JsonProperty("current_state")
+ public LifecycleCassandraState currentState()
+ {
+ return currentState;
+ }
+
+ /**
+ * @return the intended state of the Cassandra node
+ */
+ @JsonProperty("desired_state")
+ public LifecycleCassandraState desiredState()
+ {
+ return desiredState;
+ }
+
+ /**
+ * @return the status of the last lifecycle state
+ */
+ @JsonProperty("status")
+ public LifecycleStatus status()
+ {
+ return status;
+ }
+
+ /**
+ * @return message providing additional context about the last lifecycle operation
+ */
+ @JsonProperty("last_update")
+ public String lastUpdate()
+ {
+ return lastUpdate;
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (!(o instanceof LifecycleInfoResponse)) return false;
+ LifecycleInfoResponse that = (LifecycleInfoResponse) o;
+ return currentState == that.currentState && desiredState == that.desiredState && status == that.status && Objects.equals(lastUpdate, that.lastUpdate);
+ }
+
+ public int hashCode()
Review Comment:
NIT
```suggestion
@Override
public int hashCode()
```
##########
server/src/main/java/org/apache/cassandra/sidecar/handlers/LifecycleUpdateHandler.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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 com.google.inject.Singleton;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.http.HttpServerRequest;
+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 io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.common.data.LifecycleCassandraState;
+import org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.exceptions.LifecycleTaskConflictException;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleManager;
+import org.apache.cassandra.sidecar.utils.HttpExceptions;
+
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.common.data.LifecycleCassandraState.fromNodeCommandState;
+
+/**
+ * Handles {@code PUT /api/v1/cassandra/lifecycle} requests to start or stop a Cassandra node.
+ *
+ * <p> Expects a JSON payload:
+ * { "state": "start" } or { "state": "stop" }
+ * and will record the desired state. </p>
+ */
+@Singleton
+public class LifecycleUpdateHandler extends NodeCommandHandler implements AccessProtected
+{
+ private final LifecycleManager lifecycleManager;
+
+ @Inject
+ public LifecycleUpdateHandler(InstanceMetadataFetcher metadataFetcher, ExecutorPools executorPools, LifecycleManager lifecycleManager)
+ {
+ super(metadataFetcher, executorPools, null);
+ this.lifecycleManager = lifecycleManager;
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return Collections.singleton(BasicPermissions.MODIFY_LIFECYCLE.toAuthorization());
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext context,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ NodeCommandRequestPayload request)
+ {
+ LifecycleCassandraState desiredState = fromNodeCommandState(request.state());
+ executorPools.service()
+ .executeBlocking(() -> lifecycleManager.updateDesiredState(host, desiredState))
+ .onSuccess(info ->
+ {
+ switch (info.status())
+ {
+ case CONVERGED:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.OK.code())
+ .end(Json.encode(info));
+ break;
+ case CONVERGING:
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.ACCEPTED.code())
+ .end(Json.encode(info));
+ break;
+ default:
+ logger.warn("{} request failed with unexpected result. request={}, remoteAddress={}, instance={}",
+ this.getClass().getSimpleName(), request, remoteAddress, host);
+ context.response().putHeader("Content-Type", "application/json")
+ .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
+ .end(Json.encode(info));
+ }
+ })
+ .onFailure(cause -> processFailure(cause, context, host, remoteAddress, request));
+ }
+
+ protected void processFailure(Throwable cause, RoutingContext context, String host, SocketAddress remoteAddress, NodeCommandRequestPayload request)
Review Comment:
```suggestion
@Override
protected void processFailure(Throwable cause, RoutingContext context, String host, SocketAddress remoteAddress, NodeCommandRequestPayload request)
```
##########
server/src/main/java/org/apache/cassandra/sidecar/modules/LifecycleModule.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.ProvidesIntoMap;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.handlers.LifecycleInfoHandler;
+import org.apache.cassandra.sidecar.handlers.LifecycleUpdateHandler;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleProvider;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+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.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides the telemetry capability
+ */
+public class LifecycleModule extends AbstractModule
+{
+ @Provides
+ @Singleton
+ LifecycleProvider lifecycleProvider(SidecarConfiguration sidecarConfiguration)
+ {
+ LifecycleConfiguration lifecycleConfiguration = sidecarConfiguration.lifecycleConfiguration();
+ if (!lifecycleConfiguration.enabled())
+ {
+ return getNoopProvider();
+ }
+
+ ParameterizedClassConfiguration providerClass = lifecycleConfiguration.lifecycleProvider();
+ if (providerClass == null)
+ {
+ throw new ConfigurationException("Lifecycle management is enabled, but provider not set.");
+ }
+
+ if (providerClass.className().equalsIgnoreCase(ProcessLifecycleProvider.class.getName()))
+ {
+ Map<String, String> params = new HashMap<>();
Review Comment:
any reason why we copy the params here?
##########
server/src/main/java/org/apache/cassandra/sidecar/modules/LifecycleModule.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.ProvidesIntoMap;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.handlers.LifecycleInfoHandler;
+import org.apache.cassandra.sidecar.handlers.LifecycleUpdateHandler;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleProvider;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+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.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides the telemetry capability
+ */
+public class LifecycleModule extends AbstractModule
+{
+ @Provides
+ @Singleton
+ LifecycleProvider lifecycleProvider(SidecarConfiguration sidecarConfiguration)
+ {
+ LifecycleConfiguration lifecycleConfiguration = sidecarConfiguration.lifecycleConfiguration();
+ if (!lifecycleConfiguration.enabled())
+ {
+ return getNoopProvider();
+ }
+
+ ParameterizedClassConfiguration providerClass = lifecycleConfiguration.lifecycleProvider();
+ if (providerClass == null)
+ {
+ throw new ConfigurationException("Lifecycle management is enabled, but provider not set.");
+ }
+
+ if (providerClass.className().equalsIgnoreCase(ProcessLifecycleProvider.class.getName()))
Review Comment:
not to be addressed here, but we can always use reflection to instantiate these, similar to what Cassandra does.
##########
server/src/main/java/org/apache/cassandra/sidecar/lifecycle/ProcessLifecycleProvider.java:
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.lifecycle;
+
+import java.util.Map;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+
+/**
+ * A {@link LifecycleProvider} that manages Cassandra instances as OS processes.
+ * <p>
+ * This implementation is a placeholder and is not yet implemented.
+ */
+public class ProcessLifecycleProvider implements LifecycleProvider
+{
+ public ProcessLifecycleProvider(Map<String, String> params)
+ {
+ // Params unused for now
+ }
+
+ public void start(InstanceMetadata instance)
+ {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void stop(InstanceMetadata instance)
Review Comment:
```suggestion
@Override
public void stop(InstanceMetadata instance)
```
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "yifan-c (via GitHub)" <gi...@apache.org>.
yifan-c merged PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2383333331
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleCassandraState.java:
##########
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+import static org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload.State;
+
+/**
+ * Represents the lifecycle state of a Cassandra instance.
+ */
+public enum LifecycleCassandraState
Review Comment:
Good idea, I've refactored these two enums to be contained within a common `Lifecycle` data class
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2383331693
##########
client-common/src/main/java/org/apache/cassandra/sidecar/common/data/LifecycleStatus.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.cassandra.sidecar.common.data;
+
+/**
+ * Represents the lifecycle status of this instance relative to the desired state.
+ */
+public enum LifecycleStatus
Review Comment:
Refactored to `Lifecycle.OperationStatus`
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2353592918
##########
server/src/main/java/org/apache/cassandra/sidecar/modules/LifecycleModule.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.ProvidesIntoMap;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.response.LifecycleInfoResponse;
+import org.apache.cassandra.sidecar.config.LifecycleConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.handlers.LifecycleInfoHandler;
+import org.apache.cassandra.sidecar.handlers.LifecycleUpdateHandler;
+import org.apache.cassandra.sidecar.lifecycle.LifecycleProvider;
+import org.apache.cassandra.sidecar.lifecycle.ProcessLifecycleProvider;
+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.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides the telemetry capability
+ */
+public class LifecycleModule extends AbstractModule
+{
+ @Provides
+ @Singleton
+ LifecycleProvider lifecycleProvider(SidecarConfiguration sidecarConfiguration)
+ {
+ LifecycleConfiguration lifecycleConfiguration = sidecarConfiguration.lifecycleConfiguration();
+ if (!lifecycleConfiguration.enabled())
+ {
+ return getNoopProvider();
+ }
+
+ ParameterizedClassConfiguration providerClass = lifecycleConfiguration.lifecycleProvider();
+ if (providerClass == null)
+ {
+ throw new ConfigurationException("Lifecycle management is enabled, but provider not set.");
+ }
+
+ if (providerClass.className().equalsIgnoreCase(ProcessLifecycleProvider.class.getName()))
+ {
+ Map<String, String> params = new HashMap<>();
Review Comment:
No reason, simplified this section.
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
Re: [PR] CASSSIDECAR-266: Add lifecycle APIs for starting and stopping Cassandra [cassandra-sidecar]
Posted by "andresbeckruiz (via GitHub)" <gi...@apache.org>.
andresbeckruiz commented on code in PR #256:
URL: https://github.com/apache/cassandra-sidecar/pull/256#discussion_r2370094902
##########
integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/InJvmLifecycleProviderIntegrationTest.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.lifecycle;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
+import org.apache.cassandra.testing.ClusterBuilderConfiguration;
+
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+/**
+ * Tests the {@link InJvmDTestLifecycleProvider} to ensure it can start and stop a Cassandra node
+ */
+public class InJvmLifecycleProviderIntegrationTest extends SharedClusterSidecarIntegrationTestBase
+{
+ static final InstanceMetadata LOCALHOST_METADATA = mock(InstanceMetadata.class);
+ private static final String JVM_LIFECYCLE_TEST_MIN_VERSION = "4.1";
+
+ @BeforeAll
+ static void beforeAll()
+ {
+ when(LOCALHOST_METADATA.host()).thenReturn("localhost");
+ }
+
Review Comment:
Done. Added a guard against `serverWrapper == null` in `stopSidecar()`, as it is not initialized if we do the version check before provisioning the cluster and starting Sidecar.
https://github.com/apache/cassandra-sidecar/pull/256/files#diff-fcf3a316fa527c46b9367586977474dfc37a0342292a3ff6168898c00ef9a022R441-R444
--
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-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org