From da8d906c53a886ecbb3851a90e033f6fa4727e98 Mon Sep 17 00:00:00 2001 From: Olivier Michallat Date: Wed, 30 Aug 2023 16:54:29 -0700 Subject: [PATCH] Add operation to get active compactions (fixes #364) (#365) --- .../com/datastax/mgmtapi/NodeOpsProvider.java | 6 + management-api-server/doc/openapi.json | 67 ++++++ .../mgmtapi/resources/models/Compaction.java | 191 ++++++++++++++++++ .../resources/v1/TableOpsResources.java | 34 ++++ 4 files changed, 298 insertions(+) create mode 100644 management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java diff --git a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java index e0719b24..d4cd749a 100644 --- a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java +++ b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java @@ -381,6 +381,12 @@ public String forceKeyspaceCompaction( return submitJob(OperationType.COMPACTION.name(), compactionOperation, async); } + @Rpc(name = "getCompactions") + public List> getCompactions() { + logger.debug("Getting active compactions"); + return ShimLoader.instance.get().getCompactionManager().getCompactions(); + } + @Rpc(name = "garbageCollect") public void garbageCollect( @RpcParam(name = "tombstoneOption") String tombstoneOption, diff --git a/management-api-server/doc/openapi.json b/management-api-server/doc/openapi.json index bd9d21f7..cc1eb308 100644 --- a/management-api-server/doc/openapi.json +++ b/management-api-server/doc/openapi.json @@ -1657,6 +1657,27 @@ "summary" : "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables" } }, + "/api/v1/ops/tables/compactions" : { + "get" : { + "operationId" : "getCompactions", + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Compaction" + } + } + } + }, + "description" : "Compactions" + } + }, + "summary" : "Returns active compactions" + } + }, "/api/v1/ops/tables/scrub" : { "post" : { "operationId" : "scrub_1", @@ -1780,6 +1801,52 @@ }, "required" : [ "keyspace_name", "split_output", "user_defined" ] }, + "Compaction" : { + "type" : "object", + "properties" : { + "columnfamily" : { + "type" : "string" + }, + "compactionId" : { + "type" : "string" + }, + "completed" : { + "type" : "integer", + "format" : "int64" + }, + "description" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "keyspace" : { + "type" : "string" + }, + "operationId" : { + "type" : "string" + }, + "operationType" : { + "type" : "string" + }, + "sstables" : { + "type" : "string" + }, + "targetDirectory" : { + "type" : "string" + }, + "taskType" : { + "type" : "string" + }, + "total" : { + "type" : "integer", + "format" : "int64" + }, + "unit" : { + "type" : "string" + } + } + }, "CreateOrAlterKeyspaceRequest" : { "type" : "object", "properties" : { diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java new file mode 100644 index 00000000..6c648a92 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java @@ -0,0 +1,191 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.Objects; + +/** + * Describes the state of an active compaction running on the server. + * + *

Some fields are specific to certain Cassandra versions, this is indicated in their comment. + */ +public class Compaction { + + // Note: for simplicity, we use the same keys in our JSON payload as the map returned by + // Cassandra. These constants are used for both, do not change them or the corresponding JSON + // fields will always be empty. + private static final String ID_KEY = "id"; + private static final String KEYSPACE_KEY = "keyspace"; + private static final String COLUMN_FAMILY_KEY = "columnfamily"; + private static final String COMPLETED_KEY = "completed"; + private static final String TOTAL_KEY = "total"; + private static final String TASK_TYPE_KEY = "taskType"; + private static final String UNIT_KEY = "unit"; + private static final String COMPACTION_ID_KEY = "compactionId"; + private static final String SSTABLES_KEY = "sstables"; + private static final String TARGET_DIRECTORY_KEY = "targetDirectory"; + private static final String OPERATION_TYPE_KEY = "operationType"; + private static final String OPERATION_ID_KEY = "operationId"; + private static final String DESCRIPTION_KEY = "description"; + + @JsonProperty(ID_KEY) + public final String id; + + @JsonProperty(KEYSPACE_KEY) + public final String keyspace; + + @JsonProperty(COLUMN_FAMILY_KEY) + public final String columnFamily; + + @JsonProperty(COMPLETED_KEY) + public final Long completed; + + @JsonProperty(TOTAL_KEY) + public final Long total; + + /** Only present in OSS Cassandra. */ + @JsonProperty(TASK_TYPE_KEY) + public final String taskType; + + @JsonProperty(UNIT_KEY) + public final String unit; + + /** Only present in OSS Cassandra. */ + @JsonProperty(COMPACTION_ID_KEY) + public final String compactionId; + + /** Only present in OSS Cassandra 4 or above. */ + @JsonProperty(SSTABLES_KEY) + public final String ssTables; + + /** Only present in OSS Cassandra 5. */ + @JsonProperty(TARGET_DIRECTORY_KEY) + public final String targetDirectory; + + /** Only present in DSE. */ + @JsonProperty(OPERATION_TYPE_KEY) + public final String operationType; + + /** Only present in DSE. */ + @JsonProperty(OPERATION_ID_KEY) + public final String operationId; + + /** Only present in DSE 6.8. */ + @JsonProperty(DESCRIPTION_KEY) + public final String description; + + @JsonCreator + public Compaction( + @JsonProperty(ID_KEY) String id, + @JsonProperty(KEYSPACE_KEY) String keyspace, + @JsonProperty(COLUMN_FAMILY_KEY) String columnFamily, + @JsonProperty(COMPLETED_KEY) Long completed, + @JsonProperty(TOTAL_KEY) Long total, + @JsonProperty(TASK_TYPE_KEY) String taskType, + @JsonProperty(UNIT_KEY) String unit, + @JsonProperty(COMPACTION_ID_KEY) String compactionId, + @JsonProperty(SSTABLES_KEY) String ssTables, + @JsonProperty(TARGET_DIRECTORY_KEY) String targetDirectory, + @JsonProperty(OPERATION_TYPE_KEY) String operationType, + @JsonProperty(OPERATION_ID_KEY) String operationId, + @JsonProperty(DESCRIPTION_KEY) String description) { + + this.id = id; + this.keyspace = keyspace; + this.columnFamily = columnFamily; + this.completed = completed; + this.total = total; + this.taskType = taskType; + this.unit = unit; + this.compactionId = compactionId; + this.ssTables = ssTables; + this.targetDirectory = targetDirectory; + this.operationId = operationId; + this.operationType = operationType; + this.description = description; + } + + public static Compaction fromMap(Map m) { + return new Compaction( + m.get(ID_KEY), + m.get(KEYSPACE_KEY), + m.get(COLUMN_FAMILY_KEY), + parseLongOrNull(m.get(COMPLETED_KEY)), + parseLongOrNull(m.get(TOTAL_KEY)), + m.get(TASK_TYPE_KEY), + m.get(UNIT_KEY), + m.get(COMPACTION_ID_KEY), + m.get(SSTABLES_KEY), + m.get(TARGET_DIRECTORY_KEY), + m.get(OPERATION_TYPE_KEY), + m.get(OPERATION_ID_KEY), + m.get(DESCRIPTION_KEY)); + } + + private static Long parseLongOrNull(String s) { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Compaction that = (Compaction) o; + return Objects.equals(id, that.id) + && Objects.equals(keyspace, that.keyspace) + && Objects.equals(columnFamily, that.columnFamily) + && Objects.equals(completed, that.completed) + && Objects.equals(total, that.total) + && Objects.equals(taskType, that.taskType) + && Objects.equals(unit, that.unit) + && Objects.equals(compactionId, that.compactionId) + && Objects.equals(ssTables, that.ssTables) + && Objects.equals(targetDirectory, that.targetDirectory) + && Objects.equals(operationType, that.operationType) + && Objects.equals(operationId, that.operationId) + && Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + keyspace, + columnFamily, + completed, + total, + taskType, + unit, + compactionId, + ssTables, + targetDirectory, + operationType, + operationId, + description); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException je) { + return String.format("Unable to format compaction (%s)", je.getMessage()); + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java index 6b29de65..e48e080c 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java @@ -9,11 +9,13 @@ import com.datastax.mgmtapi.resources.common.BaseResources; import com.datastax.mgmtapi.resources.helpers.ResponseTools; import com.datastax.mgmtapi.resources.models.CompactRequest; +import com.datastax.mgmtapi.resources.models.Compaction; import com.datastax.mgmtapi.resources.models.KeyspaceRequest; import com.datastax.mgmtapi.resources.models.ScrubRequest; import com.datastax.mgmtapi.resources.models.Table; import com.datastax.oss.driver.api.core.cql.ResultSet; import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; import com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -24,6 +26,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -39,6 +43,9 @@ @Path("/api/v1/ops/tables") public class TableOpsResources extends BaseResources { + private static final GenericType>> LIST_OF_MAP_OF_STRINGS = + GenericType.listOf(GenericType.mapOf(String.class, String.class)); + public TableOpsResources(ManagementApplication application) { super(application); } @@ -236,6 +243,33 @@ public Response compact(CompactRequest compactRequest) { }); } + @GET + @Path("/compactions") + @Operation(summary = "Returns active compactions", operationId = "getCompactions") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiResponse( + responseCode = "200", + description = "Compactions", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = Compaction.class)))) + public Response getCompactions() { + return handle( + () -> { + ResultSet result = + app.cqlService.executeCql(app.dbUnixSocketFile, "CALL NodeOps.getCompactions()"); + Row row = result.one(); + assert row != null; + List compactions = + row.get(0, LIST_OF_MAP_OF_STRINGS).stream() + .map(Compaction::fromMap) + .collect(Collectors.toList()); + return Response.ok(compactions, MediaType.APPLICATION_JSON).build(); + }); + } + @GET @Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON}) @ApiResponse(