This is an automated email from the ASF dual-hosted git repository.
ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new 1b6052a CASSANDRASC-95 Add sidecar client changes for restore from S3
1b6052a is described below
commit 1b6052a8c3b04fca48f82e887c12bcfb2bae0bdb
Author: Yifan Cai <[email protected]>
AuthorDate: Fri Jan 19 10:52:50 2024 -0800
CASSANDRASC-95 Add sidecar client changes for restore from S3
Patch by Saranya Krishnakumar, Yifan Cai; Reviewed by Francisco Guerrero
for CASSANDRASC-95
Co-authored-by: Yifan Cai <[email protected]>
Co-authored-by: Saranya Krishnakumar <[email protected]>
---
CHANGES.txt | 1 +
client/build.gradle | 1 +
.../cassandra/sidecar/client/SidecarClient.java | 84 +++++++++-
.../client/SidecarClientBlobRestoreExtension.java | 96 +++++++++++
.../client/request/AbortRestoreJobRequest.java | 56 +++++++
.../client/request/CreateRestoreJobRequest.java | 65 ++++++++
.../request/CreateRestoreJobSliceRequest.java | 77 +++++++++
.../sidecar/client/request/JsonPayloadRequest.java | 30 ++++
.../client/request/RestoreJobSummaryRequest.java | 57 +++++++
.../client/request/UpdateRestoreJobRequest.java | 68 ++++++++
.../client/retry/CreateRestoreJobRetryPolicy.java | 62 +++++++
.../sidecar/client/SidecarClientTest.java | 53 ++++++
common/build.gradle | 3 +-
.../cassandra/sidecar/common/ApiEndpointsV1.java | 2 +
.../data/CreateRestoreJobRequestPayloadTest.java | 182 +++++++++++++++++++++
.../common/data/CreateSliceRequestPayloadTest.java | 109 ++++++++++++
.../data/UpdateRestoreJobRequestPayloadTest.java | 76 +++++++++
17 files changed, 1020 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 2cca9d6..6b5a9b9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
1.0.0
-----
+ * Add sidecar client changes for restore from S3 (CASSANDRASC-95)
* Add restore SSTables from S3 into Cassandra feature to Cassandra Sidecar
(CASSANDRASC-92)
* Define routing order for http routes (CASSANDRASC-93)
* AbstractHandler is handling the request even when it fails to extract
params (CASSANDRASC-91)
diff --git a/client/build.gradle b/client/build.gradle
index d7564cd..202a3a1 100644
--- a/client/build.gradle
+++ b/client/build.gradle
@@ -80,6 +80,7 @@ dependencies {
testImplementation('com.squareup.okhttp3:mockwebserver:4.10.0')
testImplementation(group: 'io.netty', name: 'netty-codec-http', version:
'4.1.69.Final')
+ testFixturesApi(testFixtures(project(":common")))
testFixturesImplementation(platform('org.junit:junit-bom:5.9.2'))
testFixturesImplementation('org.junit.jupiter:junit-jupiter')
testFixturesImplementation("org.assertj:assertj-core:3.24.2")
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index 7e5dbbe..3048808 100644
---
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -20,13 +20,21 @@
package org.apache.cassandra.sidecar.client;
import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.codec.http.HttpResponseStatus;
+import org.apache.cassandra.sidecar.client.request.AbortRestoreJobRequest;
+import org.apache.cassandra.sidecar.client.request.CreateRestoreJobRequest;
+import
org.apache.cassandra.sidecar.client.request.CreateRestoreJobSliceRequest;
import org.apache.cassandra.sidecar.client.request.ImportSSTableRequest;
+import org.apache.cassandra.sidecar.client.request.RestoreJobSummaryRequest;
+import org.apache.cassandra.sidecar.client.request.UpdateRestoreJobRequest;
+import org.apache.cassandra.sidecar.client.retry.CreateRestoreJobRetryPolicy;
import org.apache.cassandra.sidecar.client.retry.IgnoreConflictRetryPolicy;
import org.apache.cassandra.sidecar.client.retry.OncePerInstanceRetryPolicy;
import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
@@ -34,21 +42,26 @@ import
org.apache.cassandra.sidecar.client.retry.RunnableOnStatusCodeRetryPolicy
import org.apache.cassandra.sidecar.client.selection.InstanceSelectionPolicy;
import
org.apache.cassandra.sidecar.client.selection.RandomInstanceSelectionPolicy;
import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
import org.apache.cassandra.sidecar.common.data.HealthResponse;
import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
+import
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
import org.apache.cassandra.sidecar.common.data.RingResponse;
import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
import org.apache.cassandra.sidecar.common.data.SchemaResponse;
import org.apache.cassandra.sidecar.common.data.TimeSkewResponse;
import org.apache.cassandra.sidecar.common.data.TokenRangeReplicasResponse;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
import org.apache.cassandra.sidecar.common.utils.HttpRange;
import org.jetbrains.annotations.Nullable;
/**
* The SidecarClient class to perform requests
*/
-public class SidecarClient implements AutoCloseable
+public class SidecarClient implements AutoCloseable,
SidecarClientBlobRestoreExtension
{
private static final Logger LOGGER =
LoggerFactory.getLogger(SidecarClient.class);
@@ -443,6 +456,75 @@ public class SidecarClient implements AutoCloseable
.build());
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture<CreateRestoreJobResponsePayload>
createRestoreJob(String keyspace,
+
String table,
+
CreateRestoreJobRequestPayload payload)
+ {
+ Objects.requireNonNull(payload, "payload cannot be null");
+ return executor.executeRequestAsync(requestBuilder()
+ .retryPolicy(new
CreateRestoreJobRetryPolicy(defaultRetryPolicy))
+ .request(new
CreateRestoreJobRequest(keyspace, table, payload))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture<Void> updateRestoreJob(String keyspace,
+ String table,
+ UUID jobId,
+
UpdateRestoreJobRequestPayload payload)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+ .request(new
UpdateRestoreJobRequest(keyspace, table, jobId, payload))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture<Void> abortRestoreJob(String keyspace, String
table, UUID jobId)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+ .request(new
AbortRestoreJobRequest(keyspace, table, jobId))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture<RestoreJobSummaryResponsePayload>
restoreJobSummary(String keyspace,
+
String table,
+
UUID jobId)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+ .request(new
RestoreJobSummaryRequest(keyspace, table, jobId))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture<Void> createRestoreJobSlice(SidecarInstance
instance,
+ String keyspace,
+ String table,
+ UUID jobId,
+
CreateSliceRequestPayload payload)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+
.singleInstanceSelectionPolicy(instance)
+ .request(new
CreateRestoreJobSliceRequest(keyspace, table, jobId, payload))
+ .build());
+ }
+
/**
* Returns a copy of the request builder with the default parameters
configured for the client.
*
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
new file mode 100644
index 0000000..da752a2
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
@@ -0,0 +1,96 @@
+/*
+ * 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.client;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
+import
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
+
+/**
+ * An extension to sidecar client interface.
+ * It includes the APIs for invoking blob based restore.
+ */
+public interface SidecarClientBlobRestoreExtension
+{
+ /**
+ * Create a new restore job
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param payload request payload
+ * @return a completable future of {@link CreateRestoreJobResponsePayload}
+ */
+ CompletableFuture<CreateRestoreJobResponsePayload> createRestoreJob(String
keyspace, String table,
+
CreateRestoreJobRequestPayload payload);
+
+ /**
+ * Update an existing restore job
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId job ID of the restore job to be updated
+ * @param payload request payload
+ * @return a completable future
+ */
+ CompletableFuture<Void> updateRestoreJob(String keyspace, String table,
UUID jobId,
+ UpdateRestoreJobRequestPayload
payload);
+
+ /**
+ * Abort an existing restore job
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId job ID of the restore job to be updated
+ * @return a completable future
+ */
+ CompletableFuture<Void> abortRestoreJob(String keyspace, String table,
UUID jobId);
+
+ /**
+ * Get the summary of an existing restore job
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId job ID of the restore job to be updated
+ * @return a completable future of {@link RestoreJobSummaryResponsePayload}
+ */
+ CompletableFuture<RestoreJobSummaryResponsePayload>
restoreJobSummary(String keyspace, String table, UUID jobId);
+
+ /**
+ * Create a new slice in the restore job
+ * or check the status of restore of an existing slice identified by the
{@link CreateSliceRequestPayload}
+ *
+ * @param instance the instance where the request will be executed
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId job ID of the restore job to create slice
+ * @param payload request payload
+ * @return a completable future
+ */
+ CompletableFuture<Void> createRestoreJobSlice(SidecarInstance instance,
+ String keyspace,
+ String table,
+ UUID jobId,
+ CreateSliceRequestPayload
payload);
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.java
new file mode 100644
index 0000000..4a1e02e
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.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.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+
+/**
+ * Represents a request to abort a restore job
+ */
+public class AbortRestoreJobRequest extends Request
+{
+ /**
+ * Constructs a Sidecar request with the given {@code requestURI}.
Defaults to {@code ssl} enabled.
+ *
+ * @param keyspace the keyspace in Cassandra
+ * @param table the table name in Cassandra
+ * @param jobId a unique identifier for the job
+ */
+ public AbortRestoreJobRequest(String keyspace, String table, UUID jobId)
+ {
+ super(requestURI(keyspace, table, jobId));
+ }
+
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.POST;
+ }
+
+ static String requestURI(String keyspace, String table, UUID jobId)
+ {
+ return ApiEndpointsV1.ABORT_RESTORE_JOB_ROUTE
+ .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+ .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+ .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
new file mode 100644
index 0000000..b120389
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.client.request;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+
+/**
+ * Represents a request to create a restore job
+ */
+public class CreateRestoreJobRequest extends
DecodableRequest<CreateRestoreJobResponsePayload>
+implements JsonPayloadRequest
+{
+ private final CreateRestoreJobRequestPayload requestPayload;
+
+ /**
+ * Constructs a decodable request with the provided parameters
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param requestPayload request payload
+ */
+ public CreateRestoreJobRequest(String keyspace, String table,
CreateRestoreJobRequestPayload requestPayload)
+ {
+ super(requestURI(keyspace, table));
+ this.requestPayload = requestPayload;
+ }
+
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public Object json()
+ {
+ return requestPayload;
+ }
+
+ static String requestURI(String keyspace, String table)
+ {
+ return ApiEndpointsV1.CREATE_RESTORE_JOB_ROUTE
+ .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+ .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table);
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
new file mode 100644
index 0000000..7c40c65
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
+
+/**
+ * Represents a request to create a restore job slice
+ */
+public class CreateRestoreJobSliceRequest extends Request implements
JsonPayloadRequest
+{
+ private final CreateSliceRequestPayload payload;
+
+ /**
+ * Constructs a Sidecar request with the given {@code requestURI}
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId job ID of the restore job to create slice
+ * @param payload request payload
+ */
+ public CreateRestoreJobSliceRequest(String keyspace, String table, UUID
jobId, CreateSliceRequestPayload payload)
+ {
+ this(keyspace, table, jobId, payload, false);
+ }
+
+ // todo: drop me once the dev endpoint is promoted
+ public CreateRestoreJobSliceRequest(String keyspace, String table, UUID
jobId, CreateSliceRequestPayload payload,
+ boolean useDevApi)
+ {
+ super(requestURI(keyspace, table, jobId, useDevApi));
+ this.payload = payload;
+ }
+
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public Object json()
+ {
+ return payload;
+ }
+
+ static String requestURI(String keyspace, String table, UUID jobId,
boolean useDevApi)
+ {
+ String api = useDevApi
+ ? ApiEndpointsV1.DEV_RESTORE_JOB_SLICES_ROUTE
+ : ApiEndpointsV1.RESTORE_JOB_SLICES_ROUTE;
+ return api
+ .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+ .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+ .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
new file mode 100644
index 0000000..5a6086c
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.client.request;
+
+/**
+ * A request that contains json payload
+ */
+public interface JsonPayloadRequest
+{
+ /**
+ * @return the JSON payload for the request
+ */
+ Object json();
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
new file mode 100644
index 0000000..bd358a6
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
+
+/**
+ * Represents a request to retrieve the summary of a restore job
+ */
+public class RestoreJobSummaryRequest extends
DecodableRequest<RestoreJobSummaryResponsePayload>
+{
+ /**
+ * Constructs a Sidecar request with the given {@code requestURI}.
Defaults to {@code ssl} enabled.
+ *
+ * @param keyspace the keyspace in Cassandra
+ * @param table the table name in Cassandra
+ * @param jobId a unique identifier for the job
+ */
+ public RestoreJobSummaryRequest(String keyspace, String table, UUID jobId)
+ {
+ super(requestURI(keyspace, table, jobId));
+ }
+
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.GET;
+ }
+
+ static String requestURI(String keyspace, String table, UUID jobId)
+ {
+ return ApiEndpointsV1.RESTORE_JOB_ROUTE
+ .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+ .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+ .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
new file mode 100644
index 0000000..eba2a0e
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
+
+/**
+ * Represents a request to update a restore job
+ */
+public class UpdateRestoreJobRequest extends Request implements
JsonPayloadRequest
+{
+ private final UpdateRestoreJobRequestPayload requestPayload;
+
+ /**
+ * Constructs a Sidecar request with the given parameters.
+ *
+ * @param keyspace name of the keyspace in the cluster
+ * @param table name of the table in the cluster
+ * @param jobId a unique identifier for the job
+ * @param requestPayload request payload
+ */
+ public UpdateRestoreJobRequest(String keyspace, String table, UUID jobId,
+ UpdateRestoreJobRequestPayload
requestPayload)
+ {
+ super(requestURI(keyspace, table, jobId));
+ this.requestPayload = requestPayload;
+ }
+
+ @Override
+ public HttpMethod method()
+ {
+ return HttpMethod.PATCH;
+ }
+
+ @Override
+ public Object json()
+ {
+ return requestPayload;
+ }
+
+ static String requestURI(String keyspace, String table, UUID jobId)
+ {
+ return ApiEndpointsV1.RESTORE_JOB_ROUTE
+ .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+ .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+ .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
new file mode 100644
index 0000000..6200e7f
--- /dev/null
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * 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.client.retry;
+
+import java.util.concurrent.CompletableFuture;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.apache.cassandra.sidecar.client.HttpResponse;
+import org.apache.cassandra.sidecar.client.exception.RetriesExhaustedException;
+import org.apache.cassandra.sidecar.client.request.Request;
+
+/**
+ * A policy to handle specific status codes for the response. Delegates all
other response codes to the
+ * provided delegate
+ */
+public class CreateRestoreJobRetryPolicy extends RetryPolicy
+{
+ private final RetryPolicy delegate;
+
+ public CreateRestoreJobRetryPolicy(RetryPolicy delegate)
+ {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void onResponse(CompletableFuture<HttpResponse> responseFuture,
+ Request request,
+ HttpResponse response,
+ Throwable throwable,
+ int attempts,
+ boolean canRetryOnADifferentHost,
+ RetryAction retryAction)
+ {
+ if (response != null && (response.statusCode() ==
HttpResponseStatus.CONFLICT.code() ||
+ response.statusCode() ==
HttpResponseStatus.BAD_REQUEST.code()))
+ {
+ logger.error("Request exhausted. response={}, attempts={}",
response, attempts);
+
responseFuture.completeExceptionally(RetriesExhaustedException.of(attempts,
request, response));
+ }
+ else
+ {
+ delegate.onResponse(responseFuture, request, response, throwable,
attempts, canRetryOnADifferentHost,
+ retryAction);
+ }
+ }
+}
diff --git
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index 17e0d9f..4f5a458 100644
---
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -28,6 +28,7 @@ import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -57,9 +58,12 @@ import org.apache.cassandra.sidecar.client.retry.RetryAction;
import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
import org.apache.cassandra.sidecar.common.data.HealthResponse;
import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
+import org.apache.cassandra.sidecar.common.data.RestoreJobSecrets;
import org.apache.cassandra.sidecar.common.data.RingEntry;
import org.apache.cassandra.sidecar.common.data.RingResponse;
import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
@@ -67,12 +71,15 @@ import
org.apache.cassandra.sidecar.common.data.SchemaResponse;
import org.apache.cassandra.sidecar.common.data.TimeSkewResponse;
import org.apache.cassandra.sidecar.common.data.TokenRangeReplicasResponse;
import org.apache.cassandra.sidecar.common.utils.HttpRange;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
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.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpResponseStatus.PARTIAL_CONTENT;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -1124,6 +1131,52 @@ abstract class SidecarClientTest
validateResponseServed(ApiEndpointsV1.NODE_SETTINGS_ROUTE);
}
+ @Test
+ void testAcceptCreateRestoreJobRequest()
+ {
+ enqueue(new MockResponse()
+ .setResponseCode(OK.code())
+
.setBody("{\"jobId\":\"8e5799a4-d277-11ed-8d85-6916bb9b8056\",\"status\":\"CREATED\"}"));
+
+ UUID jobId = UUID.fromString("8e5799a4-d277-11ed-8d85-6916bb9b8056");
+ long expireAt = System.currentTimeMillis() + 10000;
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ CreateRestoreJobRequestPayload requestPayload =
CreateRestoreJobRequestPayload.builder(secrets, expireAt)
+
.jobId(jobId)
+
.build();
+ CreateRestoreJobResponsePayload responsePayload =
client.createRestoreJob("cycling",
+
"rank_by_year_and_name",
+
requestPayload)
+ .join();
+
+ assertThat(responsePayload).isNotNull();
+ assertThat(responsePayload.jobId()).isEqualTo(jobId);
+ assertThat(responsePayload.status()).isEqualTo("CREATED");
+ }
+
+ @Test
+ void testCreateRestoreJobShouldNotRetryOnDifferentHostWithBadRequest()
+ {
+ enqueue(new MockResponse()
+ .setResponseCode(BAD_REQUEST.code())
+ .setBody("{\"status\":\"Fail\"," +
+ "\"message\":\"Error while decoding values, check
your request body\"}"));
+
+ UUID jobId = UUID.fromString("8e5799a4-d277-11ed-8d85-6916bb9b8056");
+ long expireAt = System.currentTimeMillis() + 10000;
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ CreateRestoreJobRequestPayload requestPayload =
CreateRestoreJobRequestPayload.builder(secrets, expireAt)
+
.jobId(jobId)
+
.build();
+ assertThatException().isThrownBy(() ->
client.createRestoreJob("badkeyspace",
+
"bad_table",
+
requestPayload)
+ .join())
+
.withCauseInstanceOf(RetriesExhaustedException.class)
+ .withMessageContaining("Unable to complete
request '/api/v1/keyspaces/" +
+
"badkeyspace/tables/bad_table/restore-jobs' after 1 attempt");
+ }
+
private void enqueue(MockResponse response)
{
for (MockWebServer server : servers)
diff --git a/common/build.gradle b/common/build.gradle
index e9e8d08..f87fad1 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -59,7 +59,8 @@ dependencies {
compileOnly(group: 'com.datastax.cassandra', name:
'cassandra-driver-core', version: '3.11.3')
compileOnly(group: 'io.netty', name: 'netty-codec-http', version:
'4.1.69.Final')
-
testImplementation("com.google.guava:guava:${project.rootProject.guavaVersion}")
+ testImplementation(group: 'com.fasterxml.jackson.core', name:
'jackson-databind', version: "${project.jacksonVersion}")
+ testImplementation("com.google.guava:guava:${project.guavaVersion}")
testImplementation('org.mockito:mockito-inline:4.10.0')
testImplementation("org.assertj:assertj-core:3.24.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:${project.junitVersion}")
diff --git
a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index 03b631f..1df905d 100644
---
a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++
b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -102,6 +102,8 @@ public final class ApiEndpointsV1
public static final String PER_RESTORE_JOB = RESTORE_JOBS + "/" +
JOB_ID_PATH_PARAM;
public static final String CREATE_RESTORE_JOB_ROUTE = API_V1 +
PER_KEYSPACE + PER_TABLE + RESTORE_JOBS;
public static final String RESTORE_JOB_SLICES_ROUTE = API_V1 +
PER_KEYSPACE + PER_TABLE + PER_RESTORE_JOB + SLICES;
+ public static final String DEV_RESTORE_JOB_SLICES_ROUTE = API + "/dev" +
PER_KEYSPACE +
+ PER_TABLE +
PER_RESTORE_JOB + SLICES;
public static final String RESTORE_JOB_ROUTE = API_V1 + PER_KEYSPACE +
PER_TABLE + PER_RESTORE_JOB;
public static final String ABORT_RESTORE_JOB_ROUTE = API_V1 + PER_KEYSPACE
+ PER_TABLE + PER_RESTORE_JOB + ABORT;
diff --git
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
new file mode 100644
index 0000000..cf4d6b0
--- /dev/null
+++
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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 java.time.Instant;
+import java.util.Date;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CreateRestoreJobRequestPayloadTest
+{
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Test
+ void testSerDeser() throws JsonProcessingException
+ {
+ String id = "e870e5dc-d25e-11ed-afa1-0242ac120002";
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ long time = System.currentTimeMillis() + 10000;
+ Date date = Date.from(Instant.ofEpochMilli(time));
+ CreateRestoreJobRequestPayload req =
CreateRestoreJobRequestPayload.builder(secrets, time)
+
.jobId(UUID.fromString(id))
+
.jobAgent("agent")
+
.build();
+ String json = MAPPER.writeValueAsString(req);
+ CreateRestoreJobRequestPayload test = MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class);
+ assertThat(test.jobId()).hasToString(id);
+ assertThat(test.jobAgent()).isEqualTo("agent");
+ assertThat(test.secrets()).isEqualTo(secrets);
+ assertThat(test.expireAtInMillis()).isEqualTo(time);
+ assertThat(test.expireAtAsDate()).isEqualTo(date);
+
assertThat(test.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+ }
+
+ @Test
+ void testReadFromJsonFailsWithUnknownFields() throws
JsonProcessingException
+ {
+ String uuid = "e870e5dc-d25e-11ed-afa1-0242ac120002";
+ String json = "{\"jobId\":\"" + uuid + "\"," +
+ "\"jobAgent\":\"Spark Bulk Analytics\"," +
+ "\"status\":\"Completed\"," +
+ "\"expireAt\":" + System.currentTimeMillis() + 1000 +
+ ",\"secrets\":" +
MAPPER.writeValueAsString(RestoreJobSecretsGen.genRestoreJobSecrets()) + "}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(UnrecognizedPropertyException.class)
+ .hasMessageContaining("Unrecognized field \"status\"");
+ }
+
+ @Test
+ void testReadFromJsonWithInvalidSecrets()
+ {
+ String json = "{\"secrets\":" +
+
"{\"readCredentials\":{\"accessKeyId\":\"accessKeyId\"}," +
+
"\"writeCredentials\":{\"accessKeyId\":\"accessKeyId\"}}}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasMessageContaining("Cannot construct instance");
+ }
+
+ @Test
+ void testReadFromJsonWithPartialFields() throws JsonProcessingException
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) +
+ ", \"expireAt\":" + System.currentTimeMillis() + 1000 +
"}";
+ CreateRestoreJobRequestPayload req = MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class);
+ assertThat(req).isNotNull();
+ assertThat(req.jobId()).isNull();
+ assertThat(req.jobAgent()).isNull();
+ assertThat(req.secrets()).isEqualTo(secrets);
+
assertThat(req.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+ }
+
+ @Test
+ void testReadFromJsonFailsWithoutSecrets()
+ {
+ String json = "{\"expireAt\":" + System.currentTimeMillis() + 1000 +
"}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasCauseInstanceOf(NullPointerException.class)
+ .hasMessageContaining("secrets cannot be null");
+ }
+
+ @Test
+ void testReadFromJsonFailsWithOutExpireAt() throws JsonProcessingException
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) +
"}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasCauseInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("expireAt cannot be absent or a time in past");
+ }
+
+ @Test
+ void testReadFromJsonFailWithInvalidExpireAt() throws
JsonProcessingException
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) +
+ ", \"expireAt\":" + (System.currentTimeMillis() - 1000)
+ "}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasCauseInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("expireAt cannot be absent or a time in past");
+ }
+
+ @Test
+ void testReadFromJsonFailsWithInvalidJobId() throws JsonProcessingException
+ {
+ String json = "{\"jobId\":\"12951f25-d393-4158-9e90-ec0cbe05af21\"," +
+ "\"expireAt\":\"" + System.currentTimeMillis() + 1000 +
"\"," +
+ "\"secrets\":" +
MAPPER.writeValueAsString(RestoreJobSecretsGen.genRestoreJobSecrets()) + "}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class);
+ }
+
+ @Test
+ void testReadFromJsonWithoutConsistencyLevel() throws
JsonProcessingException
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ long time = System.currentTimeMillis() + 10000;
+ Date date = Date.from(Instant.ofEpochMilli(time));
+ String json = "{\"jobId\":\"e870e5dc-d25e-11ed-afa1-0242ac120002\"," +
+ "\"jobAgent\":\"agent\"," +
+ "\"expireAt\":\"" + time + "\"," +
+ "\"secrets\":" + MAPPER.writeValueAsString(secrets) +
"}";
+ CreateRestoreJobRequestPayload test = MAPPER.readValue(json,
CreateRestoreJobRequestPayload.class);
+
assertThat(test.jobId()).hasToString("e870e5dc-d25e-11ed-afa1-0242ac120002");
+ assertThat(test.jobAgent()).isEqualTo("agent");
+ assertThat(test.secrets()).isEqualTo(secrets);
+ assertThat(test.expireAtInMillis()).isEqualTo(time);
+ assertThat(test.expireAtAsDate()).isEqualTo(date);
+
assertThat(test.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+ }
+
+
+ @Test
+ void testBuilder()
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ CreateRestoreJobRequestPayload req = CreateRestoreJobRequestPayload
+ .builder(secrets,
System.currentTimeMillis() + 10000)
+ .jobAgent("agent")
+ .updateImportOptions(options -> {
+ options
+ .resetLevel(false)
+ .clearRepaired(false);
+ })
+ .build();
+ assertThat(req.secrets()).isEqualTo(secrets);
+ assertThat(req.jobAgent()).isEqualTo("agent");
+
assertThat(req.importOptions()).isEqualTo(SSTableImportOptions.defaults()
+
.resetLevel(false)
+
.clearRepaired(false));
+ }
+}
diff --git
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
new file mode 100644
index 0000000..842cf2a
--- /dev/null
+++
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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 java.math.BigInteger;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CreateSliceRequestPayloadTest
+{
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Test
+ void testReadFromJsonString() throws JsonProcessingException
+ {
+ int bucketId = 0;
+ String json = "{\"sliceId\":\"TASK_ID-RETRY_COUNT-SEQUENCE\"," +
+ "\"bucketId\": " + bucketId + "," +
+ "\"startToken\":\"1\"," +
+ "\"endToken\":\"10\"," +
+ "\"storageBucket\":\"myBucket\"," +
+ "\"storageKey\":\"/path/to/object\"," +
+ "\"sliceChecksum\":\"12321abc\"}";
+ CreateSliceRequestPayload req = MAPPER.readValue(json,
CreateSliceRequestPayload.class);
+ assertThat(req).isNotNull();
+ assertThat(req.sliceId()).isEqualTo("TASK_ID-RETRY_COUNT-SEQUENCE");
+ assertThat(req.bucketId()).isEqualTo(bucketId);
+ assertThat(req.checksum()).isEqualTo("12321abc");
+ assertThat(req.bucket()).isEqualTo("myBucket");
+ assertThat(req.key()).isEqualTo("/path/to/object");
+ assertThat(req.startToken()).isEqualTo(BigInteger.ONE);
+ assertThat(req.endToken()).isEqualTo(BigInteger.TEN);
+ assertThat(req.compressedSize())
+ .describedAs("size is not present in the json")
+ .isNull();
+ assertThat(req.uncompressedSize())
+ .describedAs("uncompressed size is not present in the json")
+ .isNull();
+ }
+
+ @Test
+ void testReadFromJsonFailsWithMissingFields()
+ {
+ String json = "{\"sliceId\":\"TASK_ID-RETRY_COUNT-SEQUENCE\"}";
+ assertThatThrownBy(() -> MAPPER.readValue(json,
CreateSliceRequestPayload.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasCauseInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid create slice request payload");
+ }
+
+ @Test
+ void testSerDeser() throws JsonProcessingException
+ {
+ int bucketId = 2;
+ CreateSliceRequestPayload req = new CreateSliceRequestPayload("id",
bucketId, "bucket", "key", "checksum",
+
BigInteger.ONE, BigInteger.TEN, 234L, 123L);
+ String json = MAPPER.writeValueAsString(req);
+ CreateSliceRequestPayload test = MAPPER.readValue(json,
CreateSliceRequestPayload.class);
+ assertThat(test.sliceId()).isEqualTo("id");
+ assertThat(test.bucketId()).isEqualTo(bucketId);
+ assertThat(test.bucket()).isEqualTo("bucket");
+ assertThat(test.key()).isEqualTo("key");
+ assertThat(test.checksum()).isEqualTo("checksum");
+ assertThat(test.startToken()).isEqualTo(BigInteger.ONE);
+ assertThat(test.endToken()).isEqualTo(BigInteger.TEN);
+ assertThat(test.uncompressedSize()).isEqualTo(234L);
+ assertThat(test.compressedSize()).isEqualTo(123L);
+ }
+
+ @Test
+ void testSerDeserWithoutOptionalFields() throws JsonProcessingException
+ {
+ CreateSliceRequestPayload req = new CreateSliceRequestPayload("id", 1,
"bucket", "key", "checksum",
+
BigInteger.ONE, BigInteger.TEN, null, null);
+ String json = MAPPER.writeValueAsString(req);
+ assertThat(json).doesNotContain("uncompressedSize")
+ .doesNotContain("compressedSize");
+ CreateSliceRequestPayload test = MAPPER.readValue(json,
CreateSliceRequestPayload.class);
+ assertThat(test.compressedSize())
+ .describedAs("size was not set in the original object")
+ .isNull();
+ assertThat(test.uncompressedSize())
+ .describedAs("uncompressed size was not set in the original object")
+ .isNull();
+ }
+}
diff --git
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.java
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.java
new file mode 100644
index 0000000..580c49a
--- /dev/null
+++
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.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.common.data;
+
+import java.time.Instant;
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UpdateRestoreJobRequestPayloadTest
+{
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test
+ void testEmptySecrets() throws JsonProcessingException
+ {
+ String json = "{\"jobAgent\":\"Spark Bulk Analytics\"}";
+ UpdateRestoreJobRequestPayload req = mapper.readValue(json,
UpdateRestoreJobRequestPayload.class);
+
+ assertThat(req).isNotNull();
+ assertThat(req.jobAgent()).isEqualTo("Spark Bulk Analytics");
+ assertThat(req.status()).isNull();
+ assertThat(req.expireAtInMillis()).isNull();
+ assertThat(req.expireAtAsDate()).isNull();
+ assertThat(req.secrets()).isNull();
+ }
+
+ @Test
+ void testExpireAt() throws JsonProcessingException
+ {
+ long time = System.currentTimeMillis() + 1000;
+ Date date = Date.from(Instant.ofEpochMilli(time));
+ String json = "{\"expireAt\":" + time + "}";
+ UpdateRestoreJobRequestPayload req = mapper.readValue(json,
UpdateRestoreJobRequestPayload.class);
+
+ assertThat(req.expireAtAsDate()).isEqualTo(date);
+ assertThat(req.expireAtInMillis()).isEqualTo(time);
+ }
+
+ @Test
+ void testJsonSerializationShouldIgnoreUnwantedFields() throws
JsonProcessingException
+ {
+ RestoreJobSecrets secrets =
RestoreJobSecretsGen.genRestoreJobSecrets();
+ UpdateRestoreJobRequestPayload payload = new
UpdateRestoreJobRequestPayload(null, secrets
+
, null, null);
+ String json = mapper.writeValueAsString(payload);
+ assertThat(json).contains(RestoreJobConstants.JOB_SECRETS)
+ .doesNotContain(RestoreJobConstants.JOB_AGENT,
+ RestoreJobConstants.JOB_EXPIRE_AT,
+ RestoreJobConstants.JOB_STATUS)
+ .doesNotContain("empty"); // ignored by @JsonIgnore
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]