diff --git a/CHANGES.md b/CHANGES.md index ed3fa83f..b21e58fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Version 1.0.0 (Not yet Released) +* Introduce REST Module for Scheduling and Managing Cassandra Repairs - Issue #771 * Create On Demand Repair Job on Agent - Issue #775 * Modify DistributedNativeConnectionProvider to Return a Map - Issue #778 * Bump Spring, Tomcat, Jackson and other dependencies to Remove Vulnerabilities in Agent - Issue #776 diff --git a/application/pom.xml b/application/pom.xml index 8521cd09..5dcaa21c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -80,6 +80,12 @@ ${project.version} + + com.ericsson.bss.cassandra.ecchronos + rest + ${project.version} + + org.springframework.boot diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java index be74c353..872aae51 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/SpringBooter.java @@ -14,13 +14,19 @@ */ package com.ericsson.bss.cassandra.ecchronos.application; +import com.ericsson.bss.cassandra.ecchronos.rest.OnDemandRepairManagementRESTImpl; +import com.ericsson.bss.cassandra.ecchronos.rest.RepairManagementRESTImpl; +import com.ericsson.bss.cassandra.ecchronos.rest.ScheduleRepairManagementRESTImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Import; @SpringBootApplication +@Import(value = { RepairManagementRESTImpl.class, ScheduleRepairManagementRESTImpl.class, + OnDemandRepairManagementRESTImpl.class }) public class SpringBooter extends SpringBootServletInitializer { private static final Logger LOG = LoggerFactory.getLogger(SpringBooter.class); diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/ECChronos.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/ECChronos.java index b25e89f1..7a71a97a 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/ECChronos.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/ECChronos.java @@ -18,13 +18,16 @@ import com.ericsson.bss.cassandra.ecchronos.application.config.repair.FileBasedRepairConfiguration; import com.ericsson.bss.cassandra.ecchronos.connection.DistributedJmxConnectionProvider; import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.impl.metrics.RepairStatsProviderImpl; import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.DefaultRepairConfigurationProvider; import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.OnDemandStatus; import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.scheduler.OnDemandRepairSchedulerImpl; import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.scheduler.RepairSchedulerImpl; import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.state.RepairStateFactoryImpl; +import com.ericsson.bss.cassandra.ecchronos.core.impl.repair.vnode.VnodeRepairStateFactoryImpl; import com.ericsson.bss.cassandra.ecchronos.core.impl.table.TimeBasedRunPolicy; import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairStatsProvider; import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.RepairScheduler; import com.ericsson.bss.cassandra.ecchronos.core.state.ReplicationState; import com.ericsson.bss.cassandra.ecchronos.core.table.ReplicatedTableProvider; @@ -48,6 +51,7 @@ public class ECChronos implements Closeable private final RepairSchedulerImpl myRepairSchedulerImpl; private final TimeBasedRunPolicy myTimeBasedRunPolicy; private final OnDemandRepairSchedulerImpl myOnDemandRepairSchedulerImpl; + private final RepairStatsProvider myRepairStatsProvider; public ECChronos( final Config configuration, @@ -119,6 +123,9 @@ public ECChronos( .withDistributedNativeConnectionProvider(nativeConnectionProvider) .withTableReferenceFactory(myECChronosInternals.getTableReferenceFactory())); + myRepairStatsProvider = new RepairStatsProviderImpl( + nativeConnectionProvider, + new VnodeRepairStateFactoryImpl(replicationState, repairHistoryService, true)); myECChronosInternals.addRunPolicy(myTimeBasedRunPolicy); } @@ -146,6 +153,12 @@ public OnDemandRepairScheduler onDemandRepairScheduler() return myOnDemandRepairSchedulerImpl; } + @Bean + public RepairStatsProvider repairStatsProvider() + { + return myRepairStatsProvider; + } + @Override public final void close() { diff --git a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java index 69f6e906..a1d20c04 100644 --- a/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java +++ b/connection.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/connection/impl/builders/DistributedJmxBuilder.java @@ -45,7 +45,7 @@ public class DistributedJmxBuilder { private static final Logger LOG = LoggerFactory.getLogger(DistributedJmxBuilder.class); private static final String JMX_FORMAT_URL = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; - private static final String JMX_JOLOKIA_FORMAT_URL = "service:jmx:jolokia://%s:%d/jolokia"; + private static final String JMX_JOLOKIA_FORMAT_URL = "service:jmx:jolokia://%s:%d/jolokia/"; private static final int DEFAULT_JOLOKIA_PORT = 8778; private static final int DEFAULT_PORT = 7199; @@ -212,7 +212,7 @@ public void reconnect(final Node node) throws EcChronosException } catch ( - AllNodesFailedException | QueryExecutionException | IOException | SecurityException e) + AllNodesFailedException | QueryExecutionException | IOException | SecurityException e) { LOG.error("Failed to create JMX connection with node {} because of {}", node.getHostId(), e.getMessage()); myEccNodesSync.updateNodeStatus(NodeStatus.UNAVAILABLE, node.getDatacenter(), node.getHostId()); diff --git a/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/metrics/RepairStatsProviderImpl.java b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/metrics/RepairStatsProviderImpl.java new file mode 100644 index 00000000..97f957bb --- /dev/null +++ b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/metrics/RepairStatsProviderImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.impl.metrics; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairStatsProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairState; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairStateFactory; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairStateUtils; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairStates; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import java.util.Collection; +import java.util.UUID; +import java.util.stream.Collectors; + +public class RepairStatsProviderImpl implements RepairStatsProvider +{ + private final DistributedNativeConnectionProvider myNativeConnectionProvider; + private final VnodeRepairStateFactory myVnodeRepairStateFactory; + + public RepairStatsProviderImpl( + final DistributedNativeConnectionProvider nativeConnectionProvider, + final VnodeRepairStateFactory vnodeRepairStateFactory) + { + myVnodeRepairStateFactory = vnodeRepairStateFactory; + myNativeConnectionProvider = nativeConnectionProvider; + } + + @Override + public final RepairStats getRepairStats( + final UUID nodeID, + final TableReference tableReference, + final long since, + final long to) + { + Node node = myNativeConnectionProvider.getNodes().get(nodeID); + VnodeRepairStates vnodeRepairStates; + vnodeRepairStates = myVnodeRepairStateFactory.calculateClusterWideState(node, tableReference, to, since); + Collection states = vnodeRepairStates.getVnodeRepairStates(); + Collection repairedStates = states.stream() + .filter(s -> isRepaired(s, since, to)) + .collect(Collectors.toList()); + double repairedRatio = states.isEmpty() ? 0 : (double) repairedStates.size() / states.size(); + return new RepairStats(tableReference.getKeyspace(), tableReference.getTable(), repairedRatio, + VnodeRepairStateUtils.getRepairTime(repairedStates)); + } + + private boolean isRepaired(final VnodeRepairState state, final long since, final long to) + { + return state.getStartedAt() >= since && state.getFinishedAt() <= to; + } +} + diff --git a/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/repair/state/RepairStateFactoryImpl.java b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/repair/state/RepairStateFactoryImpl.java index 2b74f72c..12ae807d 100644 --- a/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/repair/state/RepairStateFactoryImpl.java +++ b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/repair/state/RepairStateFactoryImpl.java @@ -21,6 +21,7 @@ import com.ericsson.bss.cassandra.ecchronos.core.state.HostStates; import com.ericsson.bss.cassandra.ecchronos.core.state.PostUpdateHook; +import com.ericsson.bss.cassandra.ecchronos.core.state.RepairHistoryProvider; import com.ericsson.bss.cassandra.ecchronos.core.state.RepairState; import com.ericsson.bss.cassandra.ecchronos.core.state.RepairStateFactory; import com.ericsson.bss.cassandra.ecchronos.core.state.ReplicaRepairGroupFactory; @@ -28,7 +29,6 @@ import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairStateFactory; import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; import com.ericsson.bss.cassandra.ecchronos.core.table.TableRepairMetrics; -import com.ericsson.bss.cassandra.ecchronos.data.repairhistory.RepairHistoryService; public final class RepairStateFactoryImpl implements RepairStateFactory { @@ -77,7 +77,7 @@ public static class Builder { private ReplicationState myReplicationState; private HostStates myHostStates; - private RepairHistoryService myRepairHistoryProvider; + private RepairHistoryProvider myRepairHistoryProvider; private TableRepairMetrics myTableRepairMetrics; /** @@ -110,7 +110,7 @@ public Builder withHostStates(final HostStates hostStates) * @param repairHistoryProvider The repair history provider. * @return Builder */ - public Builder withRepairHistoryProvider(final RepairHistoryService repairHistoryProvider) + public Builder withRepairHistoryProvider(final RepairHistoryProvider repairHistoryProvider) { myRepairHistoryProvider = repairHistoryProvider; return this; diff --git a/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/table/TableRepairJob.java b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/table/TableRepairJob.java index a54966c3..88088b6b 100644 --- a/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/table/TableRepairJob.java +++ b/core.impl/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/impl/table/TableRepairJob.java @@ -165,6 +165,8 @@ public Iterator iterator() .withTokensPerRepair(tokensPerRepair) .withRepairPolicies(getRepairPolicies()) .withRepairHistory(myRepairHistory) + .withRepairResourceFactory(getRepairLockType().getLockFactory()) + .withRepairLockFactory(REPAIR_LOCK_FACTORY) .withJobId(getId()) .withNode(myNode); diff --git a/core/pom.xml b/core/pom.xml index e11db908..4c6c4f4f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -56,6 +56,12 @@ caffeine + + + jakarta.validation + jakarta.validation-api + + org.junit.vintage diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/RepairStatsProvider.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/RepairStatsProvider.java new file mode 100644 index 00000000..b64193f2 --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/RepairStatsProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import java.util.UUID; + +public interface RepairStatsProvider +{ + RepairStats getRepairStats(UUID nodeID, TableReference tableReference, long since, long to); +} diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/OnDemandRepair.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/OnDemandRepair.java new file mode 100644 index 00000000..fd7bdf7f --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/OnDemandRepair.java @@ -0,0 +1,131 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairType; +import com.google.common.annotations.VisibleForTesting; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; +import java.util.UUID; + + +/** + * A representation of an on demand repair. + * + * Primarily used to have a type to convert to JSON. + */ +@SuppressWarnings("VisibilityModifier") +public class OnDemandRepair +{ + @NotBlank + public UUID id; + @NotBlank + public UUID hostId; + @NotBlank + public String keyspace; + @NotBlank + public String table; + @NotBlank + public OnDemandRepairJobView.Status status; + @NotBlank + @Min(0) + @Max(1) + public double repairedRatio; + @NotBlank + @Min(-1) + public long completedAt; + @NotBlank + public RepairType repairType; + + public OnDemandRepair() + { + } + + @VisibleForTesting + public OnDemandRepair(final UUID theId, + final UUID theHostId, + final String theKeyspace, + final String theTable, + final OnDemandRepairJobView.Status theStatus, + final double theRepairedRatio, + final long wasCompletedAt, + final RepairType theRepairType) + { + this.id = theId; + this.hostId = theHostId; + this.keyspace = theKeyspace; + this.table = theTable; + this.status = theStatus; + this.repairedRatio = theRepairedRatio; + this.completedAt = wasCompletedAt; + this.repairType = theRepairType; + } + + + public OnDemandRepair(final OnDemandRepairJobView repairJobView) + { + this.id = repairJobView.getId(); + this.hostId = repairJobView.getHostId(); + this.keyspace = repairJobView.getTableReference().getKeyspace(); + this.table = repairJobView.getTableReference().getTable(); + this.status = repairJobView.getStatus(); + this.repairedRatio = repairJobView.getProgress(); + this.completedAt = repairJobView.getCompletionTime(); + this.repairType = repairJobView.getRepairType(); + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + OnDemandRepair that = (OnDemandRepair) o; + return id.equals(that.id) + && hostId.equals(that.hostId) + && keyspace.equals(that.keyspace) + && table.equals(that.table) + && status == that.status + && Double.compare(that.repairedRatio, repairedRatio) == 0 + && completedAt == that.completedAt + && repairType.equals(that.repairType); + } + + /** + * Hash code representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects.hash(id, hostId, keyspace, table, repairedRatio, status, completedAt, repairType); + } +} diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairInfo.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairInfo.java new file mode 100644 index 00000000..0c43d0b6 --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairInfo.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import java.util.Objects; + +@SuppressWarnings("VisibilityModifier") +public class RepairInfo +{ + @NotBlank + @Min(0) + public long sinceInMs; + @NotBlank + @Min(0) + public long toInMs; + @NotBlank + public List repairStats; + + public RepairInfo(final long since, final long to, final List theRepairStats) + { + this.sinceInMs = since; + this.toInMs = to; + this.repairStats = theRepairStats; + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + RepairInfo that = (RepairInfo) o; + return sinceInMs == that.sinceInMs + && toInMs == that.toInMs + && repairStats.size() == that.repairStats.size() + && repairStats.containsAll(that.repairStats); + } + + /** + * Hash representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects.hash(sinceInMs, toInMs, repairStats); + } +} + diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairStats.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairStats.java new file mode 100644 index 00000000..d6e3d055 --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/RepairStats.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +@SuppressWarnings("VisibilityModifier") +public class RepairStats +{ + @NotBlank + public String keyspace; + @NotBlank + public String table; + @NotBlank + @Min(0) + @Max(1) + public double repairedRatio; + @NotBlank + @Min(0) + public long repairTimeTakenMs; + + public RepairStats(final String theKeyspace, + final String theTable, + final double theRepairedRatio, + final long theRepairTimeTakenMs) + { + this.keyspace = theKeyspace; + this.table = theTable; + this.repairedRatio = theRepairedRatio; + this.repairTimeTakenMs = theRepairTimeTakenMs; + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + RepairStats that = (RepairStats) o; + return Double.compare(that.repairedRatio, repairedRatio) == 0 + && repairTimeTakenMs == that.repairTimeTakenMs + && keyspace.equals(that.keyspace) + && table.equals(that.table); + } + + /** + * Hash representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects.hash(keyspace, table, repairedRatio, repairTimeTakenMs); + } +} + diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/Schedule.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/Schedule.java new file mode 100644 index 00000000..fac90e7e --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/Schedule.java @@ -0,0 +1,159 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.ScheduledRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairStates; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairType; +import com.google.common.annotations.VisibleForTesting; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * A representation of a schedule. + * + * Primarily used to have a type to convert to JSON. + */ +@SuppressWarnings("VisibilityModifier") +public class Schedule +{ + @NotBlank + public UUID id; + @NotBlank + public String keyspace; + @NotBlank + public String table; + @NotBlank + public ScheduledRepairJobView.Status status; + @NotBlank + @Min(0) + @Max(1) + public double repairedRatio; + @NotBlank + public long lastRepairedAtInMs; + @NotBlank + public long nextRepairInMs; + @NotBlank + public ScheduleConfig config; + @NotBlank + public RepairType repairType; + public List virtualNodeStates; + + public Schedule() + { + } + + @VisibleForTesting + public Schedule(final UUID theId, + final String theKeyspace, + final String theTable, + final ScheduledRepairJobView.Status theStatus, + final double theRepairedRatio, + final long theLastRepairedAtInMs, + final long theNextRepairInMs, + final ScheduleConfig theConfig, + final RepairType theRepairType) + { + this.id = theId; + this.keyspace = theKeyspace; + this.table = theTable; + this.status = theStatus; + this.repairedRatio = theRepairedRatio; + this.lastRepairedAtInMs = theLastRepairedAtInMs; + this.nextRepairInMs = theNextRepairInMs; + this.config = theConfig; + this.virtualNodeStates = Collections.emptyList(); + this.repairType = theRepairType; + } + + public Schedule(final ScheduledRepairJobView repairJobView) + { + this.id = repairJobView.getId(); + this.keyspace = repairJobView.getTableReference().getKeyspace(); + this.table = repairJobView.getTableReference().getTable(); + this.status = repairJobView.getStatus(); + this.repairedRatio = repairJobView.getProgress(); + this.lastRepairedAtInMs = repairJobView.getCompletionTime(); + this.nextRepairInMs = repairJobView.getNextRepair(); + this.config = new ScheduleConfig(repairJobView); + this.virtualNodeStates = Collections.emptyList(); + this.repairType = repairJobView.getRepairType(); + } + + public Schedule(final ScheduledRepairJobView repairJobView, final boolean full) + { + this(repairJobView); + if (full) + { + long repairedAfter + = System.currentTimeMillis() - repairJobView.getRepairConfiguration().getRepairIntervalInMs(); + VnodeRepairStates vnodeRepairStates = repairJobView.getRepairStateSnapshot().getVnodeRepairStates(); + + this.virtualNodeStates = vnodeRepairStates.getVnodeRepairStates().stream() + .map(vrs -> VirtualNodeState.convert(vrs, repairedAfter)) + .collect(Collectors.toList()); + } + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + Schedule that = (Schedule) o; + return lastRepairedAtInMs == that.lastRepairedAtInMs + && Double.compare(that.repairedRatio, repairedRatio) == 0 + && nextRepairInMs == that.nextRepairInMs + && keyspace.equals(that.keyspace) + && table.equals(that.table) + && status == that.status + && id.equals(that.id) + && config.equals(that.config) + && virtualNodeStates.equals(that.virtualNodeStates) + && repairType.equals(that.repairType); + } + + /** + * Hash representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects.hash(id, keyspace, table, lastRepairedAtInMs, repairedRatio, + status, nextRepairInMs, config, virtualNodeStates, repairType); + } +} + diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/ScheduleConfig.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/ScheduleConfig.java new file mode 100644 index 00000000..9783c0ae --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/ScheduleConfig.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.config.RepairConfiguration; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.ScheduledRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairParallelism; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +/** + * A representation of a table repair configuration. + * + * Primarily used to have a type to convert to JSON. + */ +@SuppressWarnings("VisibilityModifier") +public class ScheduleConfig +{ + @NotBlank + @Min(0) + public long intervalInMs; + @NotBlank + @Min(0) + public double unwindRatio; + @NotBlank + @Min(0) + public long warningTimeInMs; + @NotBlank + @Min(0) + public long errorTimeInMs; + @NotBlank + public RepairParallelism parallelism; + + public ScheduleConfig() + { + } + + public ScheduleConfig(final ScheduledRepairJobView repairJobView) + { + RepairConfiguration config = repairJobView.getRepairConfiguration(); + + this.intervalInMs = config.getRepairIntervalInMs(); + this.unwindRatio = config.getRepairUnwindRatio(); + this.warningTimeInMs = config.getRepairWarningTimeInMs(); + this.errorTimeInMs = config.getRepairErrorTimeInMs(); + this.parallelism = config.getRepairParallelism(); + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + ScheduleConfig that = (ScheduleConfig) o; + return intervalInMs == that.intervalInMs + && Double.compare(that.unwindRatio, unwindRatio) == 0 + && warningTimeInMs == that.warningTimeInMs + && errorTimeInMs == that.errorTimeInMs + && parallelism == that.parallelism; + } + + /** + * Hash representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects + .hash(intervalInMs, unwindRatio, warningTimeInMs, errorTimeInMs, parallelism); + } +} + diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/VirtualNodeState.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/VirtualNodeState.java new file mode 100644 index 00000000..2539f032 --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/VirtualNodeState.java @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.core.repair.types; + +import com.ericsson.bss.cassandra.ecchronos.core.metadata.DriverNode; +import com.ericsson.bss.cassandra.ecchronos.core.state.VnodeRepairState; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.net.InetAddress; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A representation of a virtual node state. + * + * Primarily used to to have a type to convert to JSON. + */ +@SuppressWarnings("VisibilityModifier") +public class VirtualNodeState +{ + @NotBlank + @Min(Long.MIN_VALUE) + public long startToken; + @NotBlank + @Max(Long.MAX_VALUE) + public long endToken; + @NotBlank + public Set replicas; + @NotBlank + @Min(0) + public long lastRepairedAtInMs; + @NotBlank + public boolean repaired; + + public VirtualNodeState() + { + } + + public VirtualNodeState(final long theStartToken, + final long theEndToken, + final Set theReplicas, + final long wasLastRepairedAtInMs, + final boolean isRepaired) + { + this.startToken = theStartToken; + this.endToken = theEndToken; + this.replicas = theReplicas; + this.lastRepairedAtInMs = wasLastRepairedAtInMs; + this.repaired = isRepaired; + } + + public static VirtualNodeState convert(final VnodeRepairState vnodeRepairState, final long repairedAfter) + { + long startToken = vnodeRepairState.getTokenRange().start; + long endToken = vnodeRepairState.getTokenRange().end; + Set replicas = vnodeRepairState + .getReplicas().stream().map(DriverNode::getPublicAddress) + .map(InetAddress::getHostAddress).collect(Collectors.toSet()); + long lastRepairedAt = vnodeRepairState.lastRepairedAt(); + boolean repaired = lastRepairedAt > repairedAfter; + + return new VirtualNodeState(startToken, endToken, replicas, lastRepairedAt, repaired); + } + + /** + * Equality. + * + * @param o The object to compare to. + * @return boolean + */ + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + VirtualNodeState that = (VirtualNodeState) o; + return startToken == that.startToken + && endToken == that.endToken + && lastRepairedAtInMs == that.lastRepairedAtInMs + && repaired == that.repaired + && replicas.equals(that.replicas); + } + + /** + * Hash representation. + * + * @return int + */ + @Override + public int hashCode() + { + return Objects.hash(startToken, endToken, replicas, lastRepairedAtInMs, repaired); + } +} + diff --git a/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/package-info.java b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/package-info.java new file mode 100644 index 00000000..2e5cdee2 --- /dev/null +++ b/core/src/main/java/com/ericsson/bss/cassandra/ecchronos/core/repair/types/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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. + */ +/** + * Contains the objects and resources for repair used in rest. + */ +package com.ericsson.bss.cassandra.ecchronos.core.repair.types; diff --git a/pom.xml b/pom.xml index 05f597f4..78dfd3ac 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ utils fault.manager fault.manager.impl + rest @@ -124,6 +125,7 @@ 6.1.0 3.1.0 5.3.1 + 2.2.25 4.18.1 @@ -378,6 +380,19 @@ ${jolokia.adapter.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation-api.version} + + + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + + org.mockito diff --git a/rest/pom.xml b/rest/pom.xml new file mode 100644 index 00000000..be3aa976 --- /dev/null +++ b/rest/pom.xml @@ -0,0 +1,79 @@ + + + + 4.0.0 + + com.ericsson.bss.cassandra.ecchronos + agent + 1.0.0-SNAPSHOT + + + rest + + + + + com.ericsson.bss.cassandra.ecchronos + core + ${project.version} + + + + com.ericsson.bss.cassandra.ecchronos + connection + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + com.fasterxml.jackson.core + jackson-databind + + + + org.junit.vintage + junit-vintage-engine + test + + + + org.mockito + mockito-core + test + + + + org.assertj + assertj-core + test + + + + + io.swagger.core.v3 + swagger-annotations + + + + \ No newline at end of file diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java new file mode 100644 index 00000000..a900b75e --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairType; +import java.util.UUID; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * On Demand Repair REST interface. + * + * Whenever the interface is changed it must be reflected in docs. + */ +public interface OnDemandRepairManagementREST +{ + /** + * Get a list of on demand repairs. Will fetch all if no keyspace or table is specified. + * + * @param keyspace The keyspace of the table (optional) + * @param table The table to get status of (optional) + * @param hostId The hostId of the on demand repair (optional) + * @return A list of JSON representations of {@link OnDemandRepair} + */ + ResponseEntity> getRepairs(String keyspace, String table, String hostId); + /** + * Get a list of on demand repairs associated with a specific id. + * + * @param id The id of the on demand repair + * @param hostId The hostId of the on demand repair (optional) + * @return A list of JSON representations of {@link OnDemandRepair} + */ + ResponseEntity> getRepairs(String id, String hostId); + + /** + * Schedule an on demand repair to be run on a specific table. + * + * @param nodeID The node to execute repair + * @param keyspace The keyspace of the table + * @param table The table + * @param repairType The type of repair (optional) + * @param isLocal If repair should be only run for the local node (optional) + * @return A JSON representation of {@link OnDemandRepair} + */ + ResponseEntity> runRepair( + UUID nodeID, String keyspace, String table, RepairType repairType, + boolean isLocal); +} + diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java new file mode 100644 index 00000000..2bd5a25a --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java @@ -0,0 +1,306 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.core.table.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReferenceFactory; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairType; +import com.ericsson.bss.cassandra.ecchronos.utils.exceptions.EcChronosException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * REST Controller for managing on-demand repair operations in ecChronos, + * allowing creation, retrieval, and filtering of repair jobs. + */ +@Tag(name = "Repair-Management", description = "Management of repairs") +@RestController +public class OnDemandRepairManagementRESTImpl implements OnDemandRepairManagementREST +{ + private final OnDemandRepairScheduler myOnDemandRepairScheduler; + + private final TableReferenceFactory myTableReferenceFactory; + + private final ReplicatedTableProvider myReplicatedTableProvider; + + @Autowired + private final DistributedNativeConnectionProvider myDistributedNativeConnectionProvider; + + public OnDemandRepairManagementRESTImpl( + final OnDemandRepairScheduler demandRepairScheduler, + final TableReferenceFactory tableReferenceFactory, + final ReplicatedTableProvider replicatedTableProvider, + final DistributedNativeConnectionProvider distributedNativeConnectionProvider) + { + myOnDemandRepairScheduler = demandRepairScheduler; + myTableReferenceFactory = tableReferenceFactory; + myReplicatedTableProvider = replicatedTableProvider; + myDistributedNativeConnectionProvider = distributedNativeConnectionProvider; + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-repairs", description = "Get manual repairs which are running/completed/failed.", + summary = "Get manual repairs.") + public final ResponseEntity> getRepairs( + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the keyspace, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the table.") + final String table, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the hostId.") + final String hostId) + { + return ResponseEntity.ok(getListOfOnDemandRepairs(keyspace, table, hostId)); + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-repairs-by-id", + description = "Get manual repairs matching the id which are running/completed/failed.", + summary = "Get manual repairs matching the id.") + public final ResponseEntity> getRepairs( + @PathVariable + @Parameter(description = "Only return repairs matching the id.") + final String id, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the hostId.") + final String hostId) + { + return ResponseEntity.ok(getListOfOnDemandRepairs(id, hostId)); + } + + @Override + @PostMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "run-repair", + description = "Run a manual repair, if 'isLocal' is not provided this will run a cluster-wide repair.", + summary = "Run a manual repair.") + public final ResponseEntity> runRepair( + @RequestParam() + @Parameter(description = "The node to run repair.") + final UUID nodeID, + @RequestParam(required = false) + @Parameter(description = "The keyspace to run repair for, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "The table to run repair for.") + final String table, + @RequestParam(required = false) + @Parameter(description = "The type of the repair, defaults to vnode.") + final RepairType repairType, + @RequestParam(required = false) + @Parameter(description = "Decides if the repair should be only for the local node, i.e not cluster-wide.") + final boolean isLocal) + { + return ResponseEntity.ok(runOnDemandRepair(nodeID, keyspace, table, getRepairTypeOrDefault(repairType), isLocal)); + } + + private RepairType getRepairTypeOrDefault(final RepairType repairType) + { + if (repairType == null) + { + return RepairType.VNODE; + } + return repairType; + } + + + private List getListOfOnDemandRepairs(final String keyspace, final String table, + final String hostId) + { + if (keyspace != null) + { + if (table != null) + { + if (hostId == null) + { + return getClusterWideOnDemandJobs(forTableOnDemand(keyspace, table)); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs(job -> keyspace.equals(job.getTableReference().getKeyspace()) + && table.equals(job.getTableReference().getTable()) + && host.equals(job.getHostId())); + } + if (hostId == null) + { + return getClusterWideOnDemandJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace())); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace()) + && host.equals(job.getHostId())); + } + else if (table == null) + { + if (hostId == null) + { + return getClusterWideOnDemandJobs(job -> true); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs(job -> host.equals(job.getHostId())); + } + throw new ResponseStatusException(BAD_REQUEST); + } + + private List getListOfOnDemandRepairs(final String id, final String hostId) + { + UUID uuid = parseIdOrThrow(id); + if (hostId == null) + { + List repairJobs = getClusterWideOnDemandJobs( + job -> uuid.equals(job.getId())); + if (repairJobs.isEmpty()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return repairJobs; + } + UUID host = parseIdOrThrow(hostId); + List repairJobs = getClusterWideOnDemandJobs(job -> uuid.equals(job.getId()) + && host.equals(job.getHostId())); + if (repairJobs.isEmpty()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return repairJobs; + } + + private List runOnDemandRepair( + final UUID nodeID, + final String keyspace, final String table, + final RepairType repairType, final boolean isLocal) + { + try + { + List onDemandRepairs; + if (keyspace != null) + { + if (table != null) + { + TableReference tableReference = myTableReferenceFactory.forTable(keyspace, table); + if (tableReference == null) + { + throw new ResponseStatusException(NOT_FOUND, + "Table " + keyspace + "." + table + " does not exist"); + } + onDemandRepairs = runLocalOrCluster(nodeID, repairType, isLocal, + Collections.singleton(myTableReferenceFactory.forTable(keyspace, table))); + } + else + { + onDemandRepairs = runLocalOrCluster(nodeID, repairType, isLocal, + myTableReferenceFactory.forKeyspace(keyspace)); + } + } + else + { + if (table != null) + { + throw new ResponseStatusException(BAD_REQUEST, "Keyspace must be provided if table is provided"); + } + onDemandRepairs = runLocalOrCluster(nodeID, repairType, isLocal, myTableReferenceFactory.forCluster()); + } + return onDemandRepairs; + } + catch (EcChronosException e) + { + throw new ResponseStatusException(NOT_FOUND, NOT_FOUND.getReasonPhrase(), e); + } + } + + private static Predicate forTableOnDemand(final String keyspace, final String table) + { + return tableView -> + { + TableReference tableReference = tableView.getTableReference(); + return tableReference.getKeyspace().equals(keyspace) + && tableReference.getTable().equals(table); + }; + } + + private List getClusterWideOnDemandJobs(final Predicate filter) + { + return myOnDemandRepairScheduler.getAllClusterWideRepairJobs().stream() + .filter(filter) + .map(OnDemandRepair::new) + .collect(Collectors.toList()); + } + + private List runLocalOrCluster( + final UUID nodeID, + final RepairType repairType, + final boolean isLocal, + final Set tables) + throws EcChronosException + { + List onDemandRepairs = new ArrayList<>(); + Node node = myDistributedNativeConnectionProvider.getNodes().get(nodeID); + for (TableReference tableReference : tables) + { + if (isLocal) + { + if (myReplicatedTableProvider.accept(node, tableReference.getKeyspace())) + { + onDemandRepairs.add(new OnDemandRepair( + myOnDemandRepairScheduler.scheduleJob(tableReference, repairType, nodeID))); + } + } + else + { + if (myReplicatedTableProvider.accept(node, tableReference.getKeyspace())) + { + List repairJobView = myOnDemandRepairScheduler.scheduleClusterWideJob( + tableReference, repairType); + onDemandRepairs.addAll( + repairJobView.stream().map(OnDemandRepair::new).collect(Collectors.toList())); + } + } + } + return onDemandRepairs; + } +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java new file mode 100644 index 00000000..7912928b --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; +import java.util.UUID; +import org.springframework.http.ResponseEntity; + +import java.time.Duration; + +/** + * Repair REST interface. + * Whenever the interface is changed it must be reflected in docs. + */ +public interface RepairManagementREST +{ + /** + * Get repair information for a specific table. + * + * @param nodeID The node to execute repair + * @param keyspace The keyspace of the table + * @param table The table + * @param since The since time (where the time window starts) + * @param duration The duration of the time window + * @return A JSON representation of {@link RepairInfo} + */ + ResponseEntity getRepairInfo( + UUID nodeID, String keyspace, String table, Long since, Duration duration); +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java new file mode 100644 index 00000000..d566cc04 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java @@ -0,0 +1,213 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairStatsProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; +import com.ericsson.bss.cassandra.ecchronos.core.table.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReferenceFactory; +import com.ericsson.bss.cassandra.ecchronos.utils.exceptions.EcChronosException; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.getDefaultDurationOrProvided; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * REST Controller for managing repair operations in ecChronos, providing endpoints to fetch repair statistics + * based on Cassandra tables, keyspaces, and time ranges. + */ +@Tag(name = "Repair-Management", description = "Management of repairs") +@RestController +@OpenAPIDefinition(info = @Info( + title = "REST API", + license = @License( + name = "Apache 2.0", + url = "https://www.apache.org/licenses/LICENSE-2.0"), + version = "1.0.0")) +public class RepairManagementRESTImpl implements RepairManagementREST +{ + @Autowired + private final TableReferenceFactory myTableReferenceFactory; + + @Autowired + private final ReplicatedTableProvider myReplicatedTableProvider; + + @Autowired + private final RepairStatsProvider myRepairStatsProvider; + + @Autowired + private final DistributedNativeConnectionProvider myDistributedNativeConnectionProvider; + + public RepairManagementRESTImpl( + final TableReferenceFactory tableReferenceFactory, + final ReplicatedTableProvider replicatedTableProvider, + final RepairStatsProvider repairStatsProvider, + final DistributedNativeConnectionProvider nativeConnectionProvider) + { + myTableReferenceFactory = tableReferenceFactory; + myReplicatedTableProvider = replicatedTableProvider; + myRepairStatsProvider = repairStatsProvider; + myDistributedNativeConnectionProvider = nativeConnectionProvider; + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairInfo", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-repair-info", + description = "Get repair information, if keyspace and table are provided while duration and since are" + + " not, the duration will default to GC_GRACE_SECONDS of the table. " + + "This operation might take time depending on the provided params since it's based on " + + "the repair history.", + summary = "Get repair information") + public final ResponseEntity getRepairInfo( + @RequestParam() + @Parameter(description = "Return repair-info matching the nodeID, mandatory parameter.") + final UUID nodeID, + @RequestParam(required = false) + @Parameter(description = "Only return repair-info matching the keyspace, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "Only return repair-info matching the table.") + final String table, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Parameter(description = "Since time, can be specified as ISO8601 date or as milliseconds since epoch." + + " Required if keyspace and table or duration is not specified.", + schema = @Schema(type = "string")) + final Long since, + @RequestParam(required = false) + @Parameter(description = "Duration, can be specified as either a simple duration like" + + " '30s' or as ISO8601 duration 'pt30s'." + + " Required if keyspace and table or since is not specified.", + schema = @Schema(type = "string")) + final Duration duration) + { + return ResponseEntity.ok(fetchRepairInfo(nodeID, keyspace, table, since, duration)); + } + + private RepairInfo fetchRepairInfo( + final UUID nodeID, + final String keyspace, final String table, + final Long since, final Duration duration + ) + { + try + { + RepairInfo repairInfo; + Duration actualDuration = duration; + if (keyspace != null) + { + if (table != null) + { + TableReference tableReference = myTableReferenceFactory.forTable(keyspace, table); + if (tableReference == null) + { + throw new ResponseStatusException(NOT_FOUND, + "Table " + keyspace + "." + table + " does not exist"); + } + actualDuration = getDefaultDurationOrProvided(tableReference, duration, since); + repairInfo = createRepairInfo(nodeID, Collections.singleton(tableReference), since, actualDuration); + } + else + { + repairInfo = createRepairInfo(nodeID, myTableReferenceFactory.forKeyspace(keyspace), since, actualDuration); + } + } + else + { + if (table != null) + { + throw new ResponseStatusException(BAD_REQUEST, "Keyspace must be provided if table is provided"); + } + repairInfo = createRepairInfo(nodeID, myTableReferenceFactory.forCluster(), since, actualDuration); + } + return repairInfo; + } + catch (EcChronosException e) + { + throw new ResponseStatusException(NOT_FOUND, NOT_FOUND.getReasonPhrase(), e); + } + } + + private RepairInfo createRepairInfo( + final UUID nodeID, + final Set tables, final Long since, + final Duration duration + ) + { + long toTime = System.currentTimeMillis(); + long sinceTime; + if (since != null) + { + sinceTime = since.longValue(); + if (duration != null) + { + toTime = sinceTime + TimeUnit.SECONDS.toMillis(duration.getSeconds()); + } + } + else if (duration != null) + { + sinceTime = toTime - TimeUnit.SECONDS.toMillis(duration.getSeconds()); + } + else + { + throw new ResponseStatusException(BAD_REQUEST, "Since or duration or both must be specified"); + } + if (toTime < sinceTime) + { + throw new ResponseStatusException(BAD_REQUEST, "'to' (" + toTime + ") is before 'since' (" + + sinceTime + ")"); + } + + List repairStats = new ArrayList<>(); + Node node = myDistributedNativeConnectionProvider.getNodes().get(nodeID); + for (TableReference table : tables) + { + if (myReplicatedTableProvider.accept(node, table.getKeyspace())) + { + repairStats.add(myRepairStatsProvider.getRepairStats(nodeID, table, sinceTime, toTime)); + } + } + return new RepairInfo(sinceTime, toTime, repairStats); + } +} + diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java new file mode 100644 index 00000000..c0aade5a --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.UUID; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * Helper class that contains methods to parse REST information. + */ +public final class RestUtils +{ + public static final String REPAIR_MANAGEMENT_ENDPOINT_PREFIX = "/repair-management"; + + private RestUtils() + { + // Utility Class + } + + public static UUID parseIdOrThrow(final String id) + { + try + { + UUID uuid = UUID.fromString(id); + return uuid; + } + catch (IllegalArgumentException e) + { + throw new ResponseStatusException(BAD_REQUEST, BAD_REQUEST.getReasonPhrase(), e); + } + } + + /** + * Fetches duration provided. + * if no duration and since are provided, it will fetch the table default + * + * @param tableReference the table to fetch the default from + * @param duration provided duration + * @param since provided since + * @return the duration + */ + public static Duration getDefaultDurationOrProvided(final TableReference tableReference, final Duration duration, + final Long since) + { + Duration singleTableDuration = duration; + if (duration == null && since == null) + { + singleTableDuration = Duration.ofSeconds(tableReference.getGcGraceSeconds()); + } + return singleTableDuration; + } +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java new file mode 100644 index 00000000..c432afd6 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * Schedule REST interface. + * + * Whenever the interface is changed it must be reflected in docs. + */ +public interface ScheduleRepairManagementREST +{ + /** + * Get a list of schedules. Will fetch all if no keyspace or table is specified. + * + * @param keyspace The keyspace of the table (optional) + * @param table The table to get status of (optional) + * @return A list of JSON representations of {@link Schedule} + */ + ResponseEntity> getSchedules(String keyspace, String table); + + /** + * Get schedule with a specific id. + * + * @param id The id of the schedule + * @param full Whether to include token range information. + * @return A JSON representation of {@link Schedule} + */ + ResponseEntity getSchedules(String id, boolean full); + + /** + * Retrieves the current status of the job being managed by this scheduler. + *@return A {@code String} representing the current status of the job. + */ + ResponseEntity getCurrentJobStatus(); +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java new file mode 100644 index 00000000..78d0f0df --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.RepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.ScheduledRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * REST controller for managing and retrieving Cassandra repair schedules and job statuses using {@link RepairScheduler}. + */ +@Tag(name = "Repair-Management", description = "Management of repairs") +@RestController +public class ScheduleRepairManagementRESTImpl implements ScheduleRepairManagementREST +{ + @Autowired + private final RepairScheduler myRepairScheduler; + + public ScheduleRepairManagementRESTImpl(final RepairScheduler repairScheduler) + { + myRepairScheduler = repairScheduler; + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/running-job", produces = MediaType.APPLICATION_JSON_VALUE) + public final ResponseEntity getCurrentJobStatus() + { + return ResponseEntity.ok(myRepairScheduler.getCurrentJobStatus()); + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/schedules", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-schedules", description = "Get schedules", summary = "Get schedules") + public final ResponseEntity> getSchedules( + @RequestParam(required = false) + @Parameter(description = "Filter schedules based on this keyspace, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "Filter schedules based on this table.") + final String table) + { + return ResponseEntity.ok(getListOfSchedules(keyspace, table)); + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/schedules/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-schedules-by-id", description = "Get schedules matching the id.", + summary = "Get schedules matching the id.") + public final ResponseEntity getSchedules( + @PathVariable + @Parameter(description = "The id of the schedule.") + final String id, + @RequestParam(required = false) + @Parameter(description = "Decides if a 'full schedule' should be returned.") + final boolean full) + { + return ResponseEntity.ok(getScheduleView(id, full)); + + } + + private List getScheduledRepairJobs(final Predicate filter) + { + return myRepairScheduler.getCurrentRepairJobs().stream() + .filter(filter) + .map(Schedule::new) + .collect(Collectors.toList()); + } + + private Schedule getScheduleView(final String id, final boolean full) + { + UUID uuid = parseIdOrThrow(id); + Optional view = myRepairScheduler.getCurrentRepairJobs().stream() + .filter(job -> job.getId().equals(uuid)).findFirst(); + if (!view.isPresent()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return new Schedule(view.get(), full); + } + + private List getListOfSchedules(final String keyspace, final String table) + { + if (keyspace != null) + { + if (table != null) + { + return getScheduledRepairJobs(forTableSchedule(keyspace, table)); + } + return getScheduledRepairJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace())); + } + else if (table == null) + { + return getScheduledRepairJobs(job -> true); + } + throw new ResponseStatusException(BAD_REQUEST); + } + + private static Predicate forTableSchedule(final String keyspace, final String table) + { + return tableView -> + { + TableReference tableReference = tableView.getTableReference(); + return tableReference.getKeyspace().equals(keyspace) + && tableReference.getTable().equals(table); + }; + } +} + diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/package-info.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/package-info.java new file mode 100644 index 00000000..add24592 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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. + */ + +/** + * Contains the REST API for ecChronos. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java new file mode 100644 index 00000000..35ebd3be --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import org.junit.Test; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.UUID; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.getDefaultDurationOrProvided; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestUtilsTest +{ + @Test + public void testParseUuid() + { + UUID uuid = UUID.randomUUID(); + UUID processedId = parseIdOrThrow(uuid.toString()); + assertThat(uuid).isEqualTo(processedId); + } + + @Test + public void testParseUuidFails() + { + String incorrectId = "incorrectId"; + catchThrowableOfType(() -> parseIdOrThrow(incorrectId), ResponseStatusException.class); + } + + @Test + public void testDurationWithSince() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, duration, since); + assertThat(processedDuration).isEqualTo(duration); + } + + @Test + public void testDefaultDurationDefault() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, null, null); + assertThat(processedDuration).isEqualTo(Duration.ofMillis(12000)); + } + + @Test + public void testDurationWithoutSince() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, duration, null); + assertThat(processedDuration).isEqualTo(duration); + } + + @Test + public void testDurationWithoutDurationReturnsNull() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long since = 1245L; + Duration processedDuration = getDefaultDurationOrProvided(tableReference, null, since); + assertThat(processedDuration).isNull(); + } +} diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java new file mode 100644 index 00000000..ed2dffd8 --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java @@ -0,0 +1,499 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.scheduler.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.core.table.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReferenceFactory; +import com.ericsson.bss.cassandra.ecchronos.utils.enums.repair.RepairType; +import com.ericsson.bss.cassandra.ecchronos.utils.exceptions.EcChronosException; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestOnDemandRepairManagementRESTImpl +{ + public static final int DEFAULT_GC_GRACE_SECONDS = 7200; + + @Mock + private OnDemandRepairScheduler myOnDemandRepairScheduler; + + @Mock + private ReplicatedTableProvider myReplicatedTableProvider; + + @Mock + private TableReferenceFactory myTableReferenceFactory; + + @Mock + private DistributedNativeConnectionProvider myDistributedNativeConnectionProvider; + + private OnDemandRepairManagementREST OnDemandRest; + + @Mock + private Node mockNode1; + + private final UUID mockNodeId1 = UUID.randomUUID(); + + @Before + public void setupMocks() + { + when(mockNode1.getHostId()).thenReturn(mockNodeId1); + + Map mockNodeMap = new HashMap<>(); + + mockNodeMap.put(mockNodeId1, mockNode1); + + when(myDistributedNativeConnectionProvider.getNodes()).thenReturn(mockNodeMap); + + OnDemandRest = new OnDemandRepairManagementRESTImpl(myOnDemandRepairScheduler, + myTableReferenceFactory, myReplicatedTableProvider, myDistributedNativeConnectionProvider); + } + + @Test + public void testGetNoRepairs() + { + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(new ArrayList<>()); + + ResponseEntity> response; + + response = OnDemandRest.getRepairs(null, null, null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", null, null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", "tb", null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", "tb", UUID.randomUUID().toString()); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void testGetRepairs() + { + UUID expectedId = UUID.randomUUID(); + UUID expectedHostId = UUID.randomUUID(); + TableReference tableReference1 = mockTableReference("ks", "tb"); + OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, 1234L, + RepairType.VNODE); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 2345L, RepairType.INCREMENTAL); + OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 3456L, RepairType.INCREMENTAL); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) + .collect(Collectors.toList()); + + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); + + ResponseEntity> response = OnDemandRest.getRepairs(null, null, null); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs(null, null, expectedHostId.toString()); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", null, null); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", null, expectedHostId.toString()); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", null); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", expectedHostId.toString()); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", UUID.randomUUID().toString()); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("wrong", "tb", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "wrong", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("wrong", "wrong", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairWithId() + { + UUID expectedId = UUID.randomUUID(); + UUID expectedHostId = UUID.randomUUID(); + TableReference tableReference1 = mockTableReference("ks", "tb"); + OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, + 1234L, RepairType.VNODE); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 2345L, RepairType.VNODE); + OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 3456L, RepairType.INCREMENTAL); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) + .collect(Collectors.toList()); + + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); + ResponseEntity> response = null; + try + { + response = OnDemandRest.getRepairs(UUID.randomUUID().toString(), expectedHostId.toString()); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(NOT_FOUND.value()); + } + assertThat(response).isNull(); + response = OnDemandRest.getRepairs(expectedId.toString(), expectedHostId.toString()); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = null; + try + { + response = OnDemandRest.getRepairs(UUID.randomUUID().toString(), null); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(NOT_FOUND.value()); + } + assertThat(response).isNull(); + response = OnDemandRest.getRepairs(expectedId.toString(), null); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepair() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference = mockTableReference("ks", "tb"); + OnDemandRepairJobView localRepairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt, + RepairType.VNODE); + List localExpectedResponse = Collections.singletonList(new OnDemandRepair(localRepairJobView)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference, RepairType.VNODE, mockNodeId1)).thenReturn(localRepairJobView); + when(myReplicatedTableProvider.accept(mockNode1, "ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, "ks", "tb", RepairType.VNODE, + true); + + assertThat(response.getBody()).isEqualTo(localExpectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + OnDemandRepairJobView repairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt, + RepairType.VNODE); + List expectedResponse = Collections.singletonList(new OnDemandRepair(repairJobView)); + + when(myOnDemandRepairScheduler.scheduleClusterWideJob(tableReference, RepairType.VNODE)).thenReturn( + Collections.singletonList(repairJobView)); + response = OnDemandRest.runRepair(mockNodeId1, "ks", "tb", RepairType.VNODE, false); + + assertThat(response.getBody()).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairIncremental() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference = mockTableReference("ks", "tb"); + OnDemandRepairJobView localRepairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt, + RepairType.INCREMENTAL); + List localExpectedResponse = Collections.singletonList(new OnDemandRepair(localRepairJobView)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(localRepairJobView); + when(myReplicatedTableProvider.accept(mockNode1, "ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, "ks", "tb", RepairType.INCREMENTAL, + true); + + assertThat(response.getBody()).isEqualTo(localExpectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + OnDemandRepairJobView repairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt, + RepairType.INCREMENTAL); + List expectedResponse = Collections.singletonList(new OnDemandRepair(repairJobView)); + + when(myOnDemandRepairScheduler.scheduleClusterWideJob(tableReference, RepairType.INCREMENTAL)).thenReturn( + Collections.singletonList(repairJobView)); + response = OnDemandRest.runRepair(mockNodeId1, "ks", "tb", RepairType.INCREMENTAL, false); + + assertThat(response.getBody()).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairOnlyKeyspace() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("ks", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt, + RepairType.VNODE); + TableReference tableReference2 = mockTableReference("ks", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt, + RepairType.VNODE); + TableReference tableReference3 = mockTableReference("ks", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt, + RepairType.VNODE); + + Set tableReferencesForKeyspace = new HashSet<>(); + tableReferencesForKeyspace.add(tableReference1); + tableReferencesForKeyspace.add(tableReference2); + tableReferencesForKeyspace.add(tableReference3); + when(myTableReferenceFactory.forKeyspace("ks")).thenReturn(tableReferencesForKeyspace); + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView3); + when(myReplicatedTableProvider.accept(mockNode1, "ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, "ks", null, null, + true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairIncrementalOnlyKeyspace() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("ks", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference2 = mockTableReference("ks", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference3 = mockTableReference("ks", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt, + RepairType.INCREMENTAL); + + Set tableReferencesForKeyspace = new HashSet<>(); + tableReferencesForKeyspace.add(tableReference1); + tableReferencesForKeyspace.add(tableReference2); + tableReferencesForKeyspace.add(tableReference3); + when(myTableReferenceFactory.forKeyspace("ks")).thenReturn(tableReferencesForKeyspace); + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView3); + when(myReplicatedTableProvider.accept(mockNode1, "ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, "ks", null, RepairType.INCREMENTAL, + true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairNoKeyspaceNoTable() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt, + RepairType.VNODE); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt, + RepairType.VNODE); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt, + RepairType.VNODE); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + OnDemandRepairJobView repairJobView4 = mockOnDemandRepairJobView(id, hostId, tableReference4, completedAt, + RepairType.VNODE); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + OnDemandRepairJobView repairJobView5 = mockOnDemandRepairJobView(id, hostId, tableReference5, completedAt, + RepairType.VNODE); + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + expectedResponse.add(new OnDemandRepair(repairJobView4)); + expectedResponse.add(new OnDemandRepair(repairJobView5)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView3); + when(myOnDemandRepairScheduler.scheduleJob(tableReference4, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView4); + when(myOnDemandRepairScheduler.scheduleJob(tableReference5, RepairType.VNODE, mockNodeId1)).thenReturn(repairJobView5); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace3")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, null, null, null, + true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairIncrementalNoKeyspaceNoTable() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + OnDemandRepairJobView repairJobView4 = mockOnDemandRepairJobView(id, hostId, tableReference4, completedAt, + RepairType.INCREMENTAL); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + OnDemandRepairJobView repairJobView5 = mockOnDemandRepairJobView(id, hostId, tableReference5, completedAt, + RepairType.INCREMENTAL); + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + expectedResponse.add(new OnDemandRepair(repairJobView4)); + expectedResponse.add(new OnDemandRepair(repairJobView5)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView3); + when(myOnDemandRepairScheduler.scheduleJob(tableReference4, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView4); + when(myOnDemandRepairScheduler.scheduleJob(tableReference5, RepairType.INCREMENTAL, mockNodeId1)).thenReturn(repairJobView5); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace3")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(mockNodeId1, null, null, + RepairType.INCREMENTAL, + true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + private OnDemandRepairJobView mockOnDemandRepairJobView(UUID id, UUID hostId, TableReference tableReference, + Long completedAt, RepairType repairType) + { + return new OnDemandRepairJobView(id, hostId, tableReference, OnDemandRepairJobView.Status.IN_QUEUE, 0.0, + completedAt, repairType); + } + + @Test + public void testRunRepairNoKeyspaceWithTable() + { + ResponseEntity> response = null; + try + { + response = OnDemandRest.runRepair(mockNodeId1, null, "table1", RepairType.VNODE, true); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(BAD_REQUEST.value()); + } + assertThat(response).isNull(); + } + + public TableReference mockTableReference(String keyspace, String table) + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getKeyspace()).thenReturn(keyspace); + when(tableReference.getTable()).thenReturn(table); + when(tableReference.getGcGraceSeconds()).thenReturn(DEFAULT_GC_GRACE_SECONDS); + when(myTableReferenceFactory.forTable(eq(keyspace), eq(table))).thenReturn(tableReference); + return tableReference; + } +} diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java new file mode 100644 index 00000000..f93a1d30 --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java @@ -0,0 +1,364 @@ +/* + * Copyright 2024 Telefonaktiebolaget LM Ericsson + * + * Licensed 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 com.ericsson.bss.cassandra.ecchronos.rest; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.ericsson.bss.cassandra.ecchronos.connection.DistributedNativeConnectionProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairStatsProvider; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; +import com.ericsson.bss.cassandra.ecchronos.core.table.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.table.TableReferenceFactory; +import com.ericsson.bss.cassandra.ecchronos.utils.exceptions.EcChronosException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestRepairManagementRESTImpl +{ + public static final int DEFAULT_GC_GRACE_SECONDS = 7200; + + @Mock + private ReplicatedTableProvider myReplicatedTableProvider; + + @Mock + private RepairStatsProvider myRepairStatsProvider; + + @Mock + private TableReferenceFactory myTableReferenceFactory; + + @Mock + private DistributedNativeConnectionProvider myDistributedNativeConnectionProvider; + + @Mock + private Node mockNode1; + + private final UUID mockNodeId1 = UUID.randomUUID(); + + private RepairManagementREST managementREST; + + @Before + public void setupMocks() + { + when(mockNode1.getHostId()).thenReturn(mockNodeId1); + + Map mockNodeMap = new HashMap<>(); + + mockNodeMap.put(mockNodeId1, mockNode1); + + when(myDistributedNativeConnectionProvider.getNodes()).thenReturn(mockNodeMap); + + managementREST = new RepairManagementRESTImpl(myTableReferenceFactory, myReplicatedTableProvider, + myRepairStatsProvider, myDistributedNativeConnectionProvider); + } + + @Test + public void testGetRepairInfo() + { + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + long to = since + durationInMs; + + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference1, since, to)).thenReturn(repairStats1); + + RepairStats repairStats2 = new RepairStats("keyspace1", "table2", 0.0, 0); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference2, since, to)).thenReturn(repairStats2); + + RepairStats repairStats3 = new RepairStats("keyspace1", "table3", 0.0, 0); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1 ,tableReference3, since, to)).thenReturn(repairStats3); + + RepairStats repairStats4 = new RepairStats("keyspace2", "table4", 0.0, 0); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1 ,tableReference4, since, to)).thenReturn(repairStats4); + + RepairStats repairStats5 = new RepairStats("keyspace3", "table5", 0.0, 0); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1 ,tableReference5, since, to)).thenReturn(repairStats5); + + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace3")).thenReturn(true); + + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + repairStats.add(repairStats2); + repairStats.add(repairStats3); + repairStats.add(repairStats4); + repairStats.add(repairStats5); + RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, null, null, since, duration); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoOnlySince() + { + long since = 1245L; + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference1), eq(since), any(long.class))).thenReturn(repairStats1); + RepairStats repairStats2 = new RepairStats("keyspace1", "table2", 0.0, 0); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference2), eq(since), any(long.class))).thenReturn(repairStats2); + RepairStats repairStats3 = new RepairStats("keyspace1", "table3", 0.0, 0); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference3), eq(since), any(long.class))).thenReturn(repairStats3); + RepairStats repairStats4 = new RepairStats("keyspace2", "table4", 0.0, 0); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference4), eq(since), any(long.class))).thenReturn(repairStats4); + RepairStats repairStats5 = new RepairStats("keyspace3", "table5", 0.0, 0); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference5), eq(since), any(long.class))).thenReturn(repairStats5); + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace3")).thenReturn(true); + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + repairStats.add(repairStats2); + repairStats.add(repairStats3); + repairStats.add(repairStats4); + repairStats.add(repairStats5); + RepairInfo expectedResponse = new RepairInfo(since, 0L, repairStats); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, null, null, since, null); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo.repairStats).containsExactlyInAnyOrderElementsOf(expectedResponse.repairStats); + assertThat(returnedRepairInfo.sinceInMs).isEqualTo(expectedResponse.sinceInMs); + assertThat(returnedRepairInfo.toInMs).isGreaterThan(since); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoOnlyDuration() + { + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference1), any(long.class), any(long.class))).thenReturn(repairStats1); + RepairStats repairStats2 = new RepairStats("keyspace1", "table2", 0.0, 0); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference2), any(long.class), any(long.class))).thenReturn(repairStats2); + RepairStats repairStats3 = new RepairStats("keyspace1", "table3", 0.0, 0); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference3), any(long.class), any(long.class))).thenReturn(repairStats3); + RepairStats repairStats4 = new RepairStats("keyspace2", "table4", 0.0, 0); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference4), any(long.class), any(long.class))).thenReturn(repairStats4); + RepairStats repairStats5 = new RepairStats("keyspace3", "table5", 0.0, 0); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + when(myRepairStatsProvider.getRepairStats(eq(mockNodeId1), eq(tableReference5), any(long.class), any(long.class))).thenReturn(repairStats5); + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace3")).thenReturn(true); + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + repairStats.add(repairStats2); + repairStats.add(repairStats3); + repairStats.add(repairStats4); + repairStats.add(repairStats5); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, null, null, null, duration); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo.repairStats).containsExactlyInAnyOrderElementsOf(repairStats); + assertThat(returnedRepairInfo.sinceInMs).isEqualTo(returnedRepairInfo.toInMs - durationInMs); + assertThat(returnedRepairInfo.toInMs).isEqualTo(returnedRepairInfo.sinceInMs + durationInMs); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoOnlyKeyspace() throws EcChronosException + { + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + long to = since + durationInMs; + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference1, since, to)).thenReturn(repairStats1); + RepairStats repairStats2 = new RepairStats("keyspace1", "table2", 0.0, 0); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference2, since, to)).thenReturn(repairStats2); + RepairStats repairStats3 = new RepairStats("keyspace1", "table3", 0.0, 0); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference3, since, to)).thenReturn(repairStats3); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + Set tableReferencesForKeyspace = new HashSet<>(); + tableReferencesForKeyspace.add(tableReference1); + tableReferencesForKeyspace.add(tableReference2); + tableReferencesForKeyspace.add(tableReference3); + when(myTableReferenceFactory.forKeyspace("keyspace1")).thenReturn(tableReferencesForKeyspace); + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + repairStats.add(repairStats2); + repairStats.add(repairStats3); + RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, "keyspace1", null, since, duration); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoKeyspaceAndTable() + { + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + long to = since + durationInMs; + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + when(myRepairStatsProvider.getRepairStats(mockNodeId1, tableReference1, since, to)).thenReturn(repairStats1); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, "keyspace1", "table1", since, duration); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoOnlyTable() + { + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + ResponseEntity response = null; + try + { + response = managementREST.getRepairInfo(mockNodeId1, null, "table1", since, duration); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(BAD_REQUEST.value()); + } + assertThat(response).isNull(); + } + + @Test + public void testGetRepairInfoForKeyspaceAndTableNoSinceOrDuration() + { + mockTableReference("keyspace1", "table1"); + RepairStats repairStats1 = new RepairStats("keyspace1", "table1", 0.0, 0); + when(myRepairStatsProvider.getRepairStats(any(UUID.class), any(TableReference.class), any(long.class), any(long.class))).thenReturn(repairStats1); + when(myReplicatedTableProvider.accept(mockNode1, "keyspace1")).thenReturn(true); + List repairStats = new ArrayList<>(); + repairStats.add(repairStats1); + ResponseEntity response = managementREST.getRepairInfo(mockNodeId1, "keyspace1", "table1", null, null); + + RepairInfo returnedRepairInfo = response.getBody(); + assertThat(returnedRepairInfo.repairStats).containsExactly(repairStats1); + assertThat(returnedRepairInfo.toInMs - returnedRepairInfo.sinceInMs).isEqualTo( + Duration.ofSeconds(DEFAULT_GC_GRACE_SECONDS).toMillis()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairInfoNoKeyspaceNoTableNoSinceOrDuration() + { + ResponseEntity response = null; + try + { + response = managementREST.getRepairInfo(mockNodeId1, null, null, null, null); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(BAD_REQUEST.value()); + } + assertThat(response).isNull(); + } + + @Test + public void testGetRepairInfoSinceBiggerThanSincePlusDuration() + { + mockTableReference("keyspace1", "table1"); + ResponseEntity response = null; + try + { + response = managementREST.getRepairInfo(mockNodeId1, "keyspace1", "table1", 0L, Duration.ofMillis(-1000)); + } + catch (ResponseStatusException e) + { + assertThat(e.getStatusCode().value()).isEqualTo(BAD_REQUEST.value()); + } + assertThat(response).isNull(); + } + + public TableReference mockTableReference(String keyspace, String table) + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getKeyspace()).thenReturn(keyspace); + when(tableReference.getTable()).thenReturn(table); + when(tableReference.getGcGraceSeconds()).thenReturn(DEFAULT_GC_GRACE_SECONDS); + when(myTableReferenceFactory.forTable(eq(keyspace), eq(table))).thenReturn(tableReference); + return tableReference; + } +}