From ff6b3ab796327a389dba67c935fa4e22cac61a60 Mon Sep 17 00:00:00 2001 From: mck Date: Mon, 25 Sep 2017 10:51:11 +1000 Subject: [PATCH] Format java files to suit checkstyle rules. Was accomplished with a combination of a hacked google-java-formatter (ignoring imports), the netbeans formatter, and manual edits. ref: https://github.com/thelastpickle/cassandra-reaper/pull/198 --- src/server/pom.xml | 4 +- src/server/src/checkstyle/java.header | 2 +- .../java/com/spotify/reaper/AppContext.java | 31 +- .../com/spotify/reaper/ReaperApplication.java | 137 +- .../ReaperApplicationConfiguration.java | 65 +- .../com/spotify/reaper/ReaperException.java | 15 +- .../ColumnFamilyStoreMBeanIterator.java | 68 + .../cassandra/JmxConnectionFactory.java | 53 +- .../spotify/reaper/cassandra/JmxProxy.java | 581 +++++---- .../reaper/cassandra/RepairStatusHandler.java | 21 +- .../cassandra/StorageServiceMBean20.java | 623 ++------- .../reaper/cassandra/package-info.java | 17 + .../java/com/spotify/reaper/core/Cluster.java | 15 +- .../com/spotify/reaper/core/NodeMetrics.java | 75 +- .../com/spotify/reaper/core/RepairRun.java | 42 +- .../spotify/reaper/core/RepairSchedule.java | 59 +- .../spotify/reaper/core/RepairSegment.java | 14 +- .../com/spotify/reaper/core/RepairUnit.java | 12 +- .../com/spotify/reaper/core/package-info.java | 17 + .../java/com/spotify/reaper/package-info.java | 17 + .../reaper/resources/ClusterResource.java | 191 +-- .../spotify/reaper/resources/CommonTools.java | 346 +++-- .../reaper/resources/PingResource.java | 21 +- .../reaper/resources/ReaperHealthCheck.java | 18 +- .../reaper/resources/RepairRunResource.java | 374 +++--- .../resources/RepairScheduleResource.java | 419 +++--- .../reaper/resources/package-info.java | 17 + .../reaper/resources/view/ClusterStatus.java | 22 +- .../reaper/resources/view/NodesStatus.java | 156 ++- .../resources/view/RepairRunStatus.java | 92 +- .../resources/view/RepairScheduleStatus.java | 65 +- .../reaper/resources/view/package-info.java | 17 + .../reaper/service/AutoSchedulingManager.java | 42 +- .../service/ClusterRepairScheduler.java | 128 +- .../spotify/reaper/service/RepairManager.java | 178 +-- .../reaper/service/RepairParameters.java | 13 +- .../spotify/reaper/service/RepairRunner.java | 350 ++--- .../com/spotify/reaper/service/RingRange.java | 45 +- .../reaper/service/SchedulingManager.java | 329 ++--- .../reaper/service/SegmentGenerator.java | 113 +- .../spotify/reaper/service/SegmentRunner.java | 854 ++++++------ .../reaper/service/SimpleCondition.java | 95 +- .../spotify/reaper/service/package-info.java | 17 + .../reaper/storage/CassandraStorage.java | 681 ++++++---- .../reaper/storage/IDistributedStorage.java | 8 +- .../com/spotify/reaper/storage/IStorage.java | 37 +- .../spotify/reaper/storage/MemoryStorage.java | 100 +- .../reaper/storage/PostgresStorage.java | 120 +- .../storage/cassandra/DateTimeCodec.java | 111 +- .../storage/cassandra/Migration003.java | 170 +-- .../storage/cassandra/package-info.java | 17 + .../spotify/reaper/storage/package-info.java | 17 + .../postgresql/BigIntegerArgumentFactory.java | 33 +- .../storage/postgresql/ClusterMapper.java | 24 +- ...ostgreSQL.java => IStoragePostgreSql.java} | 314 ++--- .../LongCollectionSQLTypeArgumentFactory.java | 40 - .../LongCollectionSqlTypeArgumentFactory.java | 56 + .../PostgresArrayArgumentFactory.java | 43 +- .../RepairParallelismArgumentFactory.java | 19 +- .../postgresql/RepairParametersMapper.java | 42 +- .../storage/postgresql/RepairRunMapper.java | 54 +- .../postgresql/RepairRunStatusMapper.java | 92 +- .../postgresql/RepairScheduleMapper.java | 69 +- .../RepairScheduleStatusMapper.java | 47 +- .../postgresql/RepairSegmentMapper.java | 32 +- .../storage/postgresql/RepairUnitMapper.java | 34 +- .../postgresql/RunStateArgumentFactory.java | 18 +- .../ScheduleStateArgumentFactory.java | 31 +- .../postgresql/StateArgumentFactory.java | 31 +- .../postgresql/UuidArgumentFactory.java | 19 +- .../reaper/storage/postgresql/UuidUtil.java | 21 +- .../storage/postgresql/package-info.java | 17 + .../com/spotify/reaper/AssertionTest.java | 40 +- ...ReaperApplicationConfigurationBuilder.java | 28 +- .../ReaperApplicationConfigurationTest.java | 26 +- .../spotify/reaper/SimpleReaperClient.java | 96 +- .../reaper/acceptance/AddClusterTest.java | 7 +- .../spotify/reaper/acceptance/BasicSteps.java | 1146 +++++++++-------- .../reaper/acceptance/ReaperCassandraIT.java | 121 +- .../spotify/reaper/acceptance/ReaperH2IT.java | 22 +- .../spotify/reaper/acceptance/ReaperIT.java | 23 +- .../reaper/acceptance/ReaperPostgresIT.java | 20 +- .../acceptance/ReaperTestJettyRunner.java | 94 +- .../reaper/acceptance/TestContext.java | 23 +- .../reaper/resources/CommonToolsTest.java | 49 +- .../resources/view/NodesStatusTest.java | 284 ++-- .../resources/view/RepairRunStatusTest.java | 21 +- .../view/RepairScheduleStatusTest.java | 24 +- .../reaper/unit/cassandra/JmxProxyTest.java | 10 +- .../spotify/reaper/unit/core/ClusterTest.java | 9 +- .../unit/resources/ClusterResourceTest.java | 74 +- .../unit/resources/RepairRunResourceTest.java | 196 +-- .../service/AutoSchedulingManagerTest.java | 25 +- .../service/ClusterRepairSchedulerTest.java | 115 +- .../reaper/unit/service/RepairRunnerTest.java | 406 +++--- .../reaper/unit/service/RingRangeTest.java | 70 +- .../unit/service/SegmentGeneratorTest.java | 156 +-- .../unit/service/SegmentRunnerTest.java | 293 +++-- .../unit/service/TestRepairConfiguration.java | 24 +- .../org.mockito.plugins.MockMaker | 1 + 100 files changed, 6422 insertions(+), 5048 deletions(-) create mode 100644 src/server/src/main/java/com/spotify/reaper/cassandra/ColumnFamilyStoreMBeanIterator.java create mode 100644 src/server/src/main/java/com/spotify/reaper/cassandra/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/core/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/resources/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/resources/view/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/service/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/storage/cassandra/package-info.java create mode 100644 src/server/src/main/java/com/spotify/reaper/storage/package-info.java rename src/server/src/main/java/com/spotify/reaper/storage/postgresql/{IStoragePostgreSQL.java => IStoragePostgreSql.java} (51%) delete mode 100644 src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSQLTypeArgumentFactory.java create mode 100644 src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSqlTypeArgumentFactory.java create mode 100644 src/server/src/main/java/com/spotify/reaper/storage/postgresql/package-info.java create mode 100644 src/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/src/server/pom.xml b/src/server/pom.xml index d34001442..ce6251092 100755 --- a/src/server/pom.xml +++ b/src/server/pom.xml @@ -167,8 +167,8 @@ org.mockito - mockito-all - 1.10.19 + mockito-core + 2.10.0 test diff --git a/src/server/src/checkstyle/java.header b/src/server/src/checkstyle/java.header index 61b17b0d9..fff8ccde9 100644 --- a/src/server/src/checkstyle/java.header +++ b/src/server/src/checkstyle/java.header @@ -1,7 +1,7 @@ /* * 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 a + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/src/server/src/main/java/com/spotify/reaper/AppContext.java b/src/server/src/main/java/com/spotify/reaper/AppContext.java index 698231418..3e774e32e 100644 --- a/src/server/src/main/java/com/spotify/reaper/AppContext.java +++ b/src/server/src/main/java/com/spotify/reaper/AppContext.java @@ -1,28 +1,43 @@ +/* + * 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.spotify.reaper; -import com.codahale.metrics.MetricRegistry; import com.spotify.reaper.cassandra.JmxConnectionFactory; import com.spotify.reaper.service.RepairManager; import com.spotify.reaper.storage.IStorage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; +import com.codahale.metrics.MetricRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** - * Single class to hold all application global interfacing objects, - * and app global options. + * Single class to hold all application global interfacing objects, and app global options. */ public final class AppContext { public static final UUID REAPER_INSTANCE_ID = UUID.randomUUID(); + public static final String REAPER_INSTANCE_ADDRESS = initialiseInstanceAddress(); + private static final String DEFAULT_INSTANCE_ADDRESS = "127.0.0.1"; private static final Logger LOG = LoggerFactory.getLogger(AppContext.class); - public static final String REAPER_INSTANCE_ADDRESS = initialiseInstanceAddress(); public final AtomicBoolean isRunning = new AtomicBoolean(true); public IStorage storage; public RepairManager repairManager; @@ -34,9 +49,9 @@ private static String initialiseInstanceAddress() { String reaperInstanceAddress; try { reaperInstanceAddress = InetAddress.getLocalHost().getHostAddress(); - } catch(UnknownHostException e) { + } catch (UnknownHostException e) { LOG.warn("Cannot get instance address", e); - reaperInstanceAddress = DEFAULT_INSTANCE_ADDRESS; + reaperInstanceAddress = DEFAULT_INSTANCE_ADDRESS; } return reaperInstanceAddress; } diff --git a/src/server/src/main/java/com/spotify/reaper/ReaperApplication.java b/src/server/src/main/java/com/spotify/reaper/ReaperApplication.java index 5a370d755..dbe90d427 100644 --- a/src/server/src/main/java/com/spotify/reaper/ReaperApplication.java +++ b/src/server/src/main/java/com/spotify/reaper/ReaperApplication.java @@ -11,32 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import javax.servlet.DispatcherType; -import javax.servlet.FilterRegistration; - -import io.dropwizard.configuration.EnvironmentVariableSubstitutor; -import io.dropwizard.configuration.SubstitutingSourceProvider; -import org.apache.cassandra.repair.RepairParallelism; -import org.eclipse.jetty.servlets.CrossOriginFilter; -import org.flywaydb.core.Flyway; -import org.joda.time.DateTimeZone; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; +package com.spotify.reaper; import com.spotify.reaper.ReaperApplicationConfiguration.JmxCredentials; import com.spotify.reaper.cassandra.JmxConnectionFactory; @@ -54,16 +30,35 @@ import com.spotify.reaper.storage.MemoryStorage; import com.spotify.reaper.storage.PostgresStorage; +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; + +import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import io.dropwizard.Application; import io.dropwizard.assets.AssetsBundle; -import org.skife.jdbi.v2.DBI; - +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; import io.dropwizard.jdbi.DBIFactory; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import io.prometheus.client.CollectorRegistry; import io.prometheus.client.dropwizard.DropwizardExports; import io.prometheus.client.exporter.MetricsServlet; +import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.flywaydb.core.Flyway; +import org.joda.time.DateTimeZone; +import org.skife.jdbi.v2.DBI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sun.misc.Signal; import sun.misc.SignalHandler; @@ -71,7 +66,7 @@ public final class ReaperApplication extends Application bootstrap) { @@ -109,7 +103,7 @@ public void initialize(Bootstrap bootstrap) { // enable using environment variables in yml files final SubstitutingSourceProvider envSourceProvider = new SubstitutingSourceProvider( - bootstrap.getConfigurationSourceProvider(), new EnvironmentVariableSubstitutor(false)); + bootstrap.getConfigurationSourceProvider(), new EnvironmentVariableSubstitutor(false)); bootstrap.setConfigurationSourceProvider(envSourceProvider); } @@ -126,7 +120,8 @@ public void run(ReaperApplicationConfiguration config, Environment environment) context.metricRegistry = environment.metrics(); CollectorRegistry.defaultRegistry.register(new DropwizardExports(environment.metrics())); - environment.admin() + environment + .admin() .addServlet("prometheusMetrics", new MetricsServlet(CollectorRegistry.defaultRegistry)) .addMapping("/prometheusMetrics"); @@ -134,8 +129,10 @@ public void run(ReaperApplicationConfiguration config, Environment environment) context.repairManager = new RepairManager(); context.repairManager.initializeThreadPool( config.getRepairRunThreadCount(), - config.getHangingRepairTimeoutMins(), TimeUnit.MINUTES, - config.getRepairManagerSchedulingIntervalSeconds(), TimeUnit.SECONDS); + config.getHangingRepairTimeoutMins(), + TimeUnit.MINUTES, + config.getRepairManagerSchedulingIntervalSeconds(), + TimeUnit.SECONDS); if (context.storage == null) { LOG.info("initializing storage of type: {}", config.getStorageType()); @@ -157,7 +154,7 @@ public void run(ReaperApplicationConfiguration config, Environment environment) context.jmxConnectionFactory.setJmxPorts(jmxPorts); } - if(config.useAddressTranslator()) { + if (config.useAddressTranslator()) { context.jmxConnectionFactory.setAddressTranslator(new EC2MultiRegionAddressTranslator()); } @@ -170,8 +167,8 @@ public void run(ReaperApplicationConfiguration config, Environment environment) // Enable cross-origin requests for using external GUI applications. if (config.isEnableCrossOrigin() || System.getProperty("enableCrossOrigin") != null) { - final FilterRegistration.Dynamic cors = environment.servlets() - .addFilter("crossOriginRequests", CrossOriginFilter.class); + final FilterRegistration.Dynamic cors + = environment.servlets().addFilter("crossOriginRequests", CrossOriginFilter.class); cors.setInitParameter("allowedOrigins", "*"); cors.setInitParameter("allowedHeaders", "X-Requested-With,Content-Type,Accept,Origin"); cors.setInitParameter("allowedMethods", "OPTIONS,GET,PUT,POST,DELETE,HEAD"); @@ -205,45 +202,45 @@ public void run(ReaperApplicationConfiguration config, Environment environment) } LOG.info("resuming pending repair runs"); - + if (context.storage instanceof IDistributedStorage) { // Allowing multiple Reaper instances to work concurrently requires // us to poll the database for running repairs regularly // only with Cassandra storage scheduler.scheduleWithFixedDelay( - () -> { + () -> { try { context.repairManager.resumeRunningRepairRuns(context); } catch (ReaperException e) { LOG.error("Couldn't resume running repair runs", e); - } - }, 0, 10, TimeUnit.SECONDS); - } - else { + } + }, + 0, + 10, + TimeUnit.SECONDS); + } else { // Storage is different than Cassandra, assuming we have a single instance context.repairManager.resumeRunningRepairRuns(context); } - } - private IStorage initializeStorage(ReaperApplicationConfiguration config, - Environment environment) throws ReaperException { + private IStorage initializeStorage(ReaperApplicationConfiguration config, Environment environment) + throws ReaperException { IStorage storage; if ("memory".equalsIgnoreCase(config.getStorageType())) { storage = new MemoryStorage(); - }else if ("cassandra".equalsIgnoreCase(config.getStorageType())) { + } else if ("cassandra".equalsIgnoreCase(config.getStorageType())) { storage = new CassandraStorage(config, environment); } else if ("database".equalsIgnoreCase(config.getStorageType())) { // create DBI instance DBI jdbi; final DBIFactory factory = new DBIFactory(); jdbi = factory.build(environment, config.getDataSourceFactory(), "postgresql"); - + // instanciate store storage = new PostgresStorage(jdbi); initDatabase(config); - - + } else { LOG.error("invalid storageType: {}", config.getStorageType()); throw new ReaperException("invalid storage type: " + config.getStorageType()); @@ -253,7 +250,7 @@ private IStorage initializeStorage(ReaperApplicationConfiguration config, } private void checkConfiguration(ReaperApplicationConfiguration config) { - LOG.debug("repairIntensity: {}",config.getRepairIntensity()); + LOG.debug("repairIntensity: {}", config.getRepairIntensity()); LOG.debug("incrementalRepair: {}", config.getIncrementalRepair()); LOG.debug("repairRunThreadCount: {}", config.getRepairRunThreadCount()); LOG.debug("segmentCount: {}", config.getSegmentCount()); @@ -268,25 +265,27 @@ void reloadConfiguration() { } private void addSignalHandlers() { - if(!System.getProperty("os.name").toLowerCase().contains("win")) { - LOG.debug("adding signal handler for SIGHUP"); - Signal.handle(new Signal("HUP"), new SignalHandler() { - @Override - public void handle(Signal signal) { - LOG.info("received SIGHUP signal: {}", signal); - reloadConfiguration(); - } - }); - } + if (!System.getProperty("os.name").toLowerCase().contains("win")) { + LOG.debug("adding signal handler for SIGHUP"); + Signal.handle(new Signal("HUP"), new SignalHandler() { + @Override + public void handle(Signal signal) { + LOG.info("received SIGHUP signal: {}", signal); + reloadConfiguration(); + } + }); + } } - + private void initDatabase(ReaperApplicationConfiguration config) throws ReaperException { Flyway flyway = new Flyway(); - flyway.setDataSource(config.getDataSourceFactory().getUrl(), config.getDataSourceFactory().getUser(), config.getDataSourceFactory().getPassword()); - if("org.h2.Driver".equals(config.getDataSourceFactory().getDriverClass())) { + flyway.setDataSource( + config.getDataSourceFactory().getUrl(), + config.getDataSourceFactory().getUser(), + config.getDataSourceFactory().getPassword()); + if ("org.h2.Driver".equals(config.getDataSourceFactory().getDriverClass())) { flyway.setLocations("/db/h2"); - } - else { + } else { flyway.setLocations("/db/postgres"); } flyway.setBaselineOnMigrate(true); diff --git a/src/server/src/main/java/com/spotify/reaper/ReaperApplicationConfiguration.java b/src/server/src/main/java/com/spotify/reaper/ReaperApplicationConfiguration.java index 96f70ed38..6c2bae010 100644 --- a/src/server/src/main/java/com/spotify/reaper/ReaperApplicationConfiguration.java +++ b/src/server/src/main/java/com/spotify/reaper/ReaperApplicationConfiguration.java @@ -11,14 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.cassandra.repair.RepairParallelism; -import org.hibernate.validator.constraints.NotEmpty; -import io.dropwizard.Configuration; -import io.dropwizard.db.DataSourceFactory; -import systems.composable.dropwizard.cassandra.CassandraFactory; +package com.spotify.reaper; import java.time.Duration; import java.util.Collections; @@ -30,8 +24,14 @@ import javax.validation.constraints.NotNull; import javax.ws.rs.DefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; +import org.apache.cassandra.repair.RepairParallelism; +import org.hibernate.validator.constraints.NotEmpty; +import systems.composable.dropwizard.cassandra.CassandraFactory; -public class ReaperApplicationConfiguration extends Configuration { +public final class ReaperApplicationConfiguration extends Configuration { @JsonProperty @NotNull @@ -72,7 +72,6 @@ public class ReaperApplicationConfiguration extends Configuration { private String enableCrossOrigin; - @JsonProperty private DataSourceFactory database = new DataSourceFactory(); @@ -138,11 +137,11 @@ public void setRepairIntensity(double repairIntensity) { } public boolean getIncrementalRepair() { - return incrementalRepair != null ? incrementalRepair : false; + return incrementalRepair != null ? incrementalRepair : false; } public void setIncrementalRepair(boolean incrementalRepair) { - this.incrementalRepair = incrementalRepair; + this.incrementalRepair = incrementalRepair; } public Integer getScheduleDaysBetween() { @@ -215,7 +214,7 @@ public void setJmxAuth(JmxCredentials jmxAuth) { } public boolean hasAutoSchedulingEnabled() { - return autoScheduling != null && autoScheduling.isEnabled(); + return autoScheduling != null && autoScheduling.isEnabled(); } public AutoSchedulingConfiguration getAutoScheduling() { @@ -260,12 +259,12 @@ public boolean useAddressTranslator() { @JsonProperty("cassandra") public CassandraFactory getCassandraFactory() { - return cassandra; + return cassandra; } @JsonProperty("cassandra") public void setCassandraFactory(CassandraFactory cassandra) { - this.cassandra = cassandra; + this.cassandra = cassandra; } public int getHangingRepairTimeoutMins() { @@ -299,6 +298,7 @@ public static final class JmxCredentials { @JsonProperty private String username; + @JsonProperty private String password; @@ -309,7 +309,6 @@ public String getUsername() { public String getPassword() { return password; } - } public static final class AutoSchedulingConfiguration { @@ -386,23 +385,27 @@ public List getExcludedKeyspaces() { @Override public String toString() { - return "AutoSchedulingConfiguration{" + - "enabled=" + enabled + - ", initialDelayPeriod=" + initialDelayPeriod + - ", periodBetweenPolls=" + periodBetweenPolls + - ", timeBeforeFirstSchedule=" + timeBeforeFirstSchedule + - ", scheduleSpreadPeriod=" + scheduleSpreadPeriod + - '}'; + return "AutoSchedulingConfiguration{" + + "enabled=" + + enabled + + ", initialDelayPeriod=" + + initialDelayPeriod + + ", periodBetweenPolls=" + + periodBetweenPolls + + ", timeBeforeFirstSchedule=" + + timeBeforeFirstSchedule + + ", scheduleSpreadPeriod=" + + scheduleSpreadPeriod + + '}'; } } - public static enum DatacenterAvailability { - /* We require direct JMX access to all nodes across all datacenters */ - ALL, - /* We require jmx access to all nodes in the local datacenter */ - LOCAL, - /* Each datacenter requires at minimum one reaper instance that has jmx access to all nodes in that datacenter */ - EACH - } - + public enum DatacenterAvailability { + /* We require direct JMX access to all nodes across all datacenters */ + ALL, + /* We require jmx access to all nodes in the local datacenter */ + LOCAL, + /* Each datacenter requires at minimum one reaper instance that has jmx access to all nodes in that datacenter */ + EACH + } } diff --git a/src/server/src/main/java/com/spotify/reaper/ReaperException.java b/src/server/src/main/java/com/spotify/reaper/ReaperException.java index bf43b60a5..3a05ef24c 100644 --- a/src/server/src/main/java/com/spotify/reaper/ReaperException.java +++ b/src/server/src/main/java/com/spotify/reaper/ReaperException.java @@ -11,19 +11,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper; -public class ReaperException extends Exception { +public final class ReaperException extends Exception { - public ReaperException(String s) { - super(s); + public ReaperException(String message) { + super(message); } - public ReaperException(Exception e) { - super(e); + public ReaperException(Exception exception) { + super(exception); } - public ReaperException(String s, Exception e) { - super(s, e); + public ReaperException(String message, Exception exception) { + super(message, exception); } } diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/ColumnFamilyStoreMBeanIterator.java b/src/server/src/main/java/com/spotify/reaper/cassandra/ColumnFamilyStoreMBeanIterator.java new file mode 100644 index 000000000..5550c695c --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/ColumnFamilyStoreMBeanIterator.java @@ -0,0 +1,68 @@ +/* + * 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.spotify.reaper.cassandra; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.Iterator; +import java.util.Map; +import javax.management.JMX; +import javax.management.MBeanServerConnection; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.cassandra.db.ColumnFamilyStoreMBean; + +/** + * This code is copied and adjusted from from NodeProbe.java from Cassandra source. + */ +final class ColumnFamilyStoreMBeanIterator implements Iterator> { + + private final Iterator resIter; + private final MBeanServerConnection mbeanServerConn; + + ColumnFamilyStoreMBeanIterator(MBeanServerConnection mbeanServerConn) + throws MalformedObjectNameException, NullPointerException, IOException { + + ObjectName query = new ObjectName("org.apache.cassandra.db:type=ColumnFamilies,*"); + resIter = mbeanServerConn.queryNames(query, null).iterator(); + this.mbeanServerConn = mbeanServerConn; + } + + static Iterator> getColumnFamilyStoreMBeanProxies( + MBeanServerConnection mbeanServerConn) throws IOException, MalformedObjectNameException { + + return new ColumnFamilyStoreMBeanIterator(mbeanServerConn); + } + + @Override + public boolean hasNext() { + return resIter.hasNext(); + } + + @Override + public Map.Entry next() { + ObjectName objectName = resIter.next(); + String keyspaceName = objectName.getKeyProperty("keyspace"); + ColumnFamilyStoreMBean cfsProxy = JMX.newMBeanProxy(mbeanServerConn, objectName, ColumnFamilyStoreMBean.class); + return new AbstractMap.SimpleImmutableEntry<>(keyspaceName, cfsProxy); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/JmxConnectionFactory.java b/src/server/src/main/java/com/spotify/reaper/cassandra/JmxConnectionFactory.java index 27ec3ab8b..a744ccf7a 100644 --- a/src/server/src/main/java/com/spotify/reaper/cassandra/JmxConnectionFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/JmxConnectionFactory.java @@ -11,12 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.cassandra; -import com.codahale.metrics.MetricRegistry; -import com.google.common.base.Optional; -import com.google.common.collect.Maps; -import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; import com.spotify.reaper.ReaperApplicationConfiguration.JmxCredentials; import com.spotify.reaper.ReaperException; import com.spotify.reaper.core.Cluster; @@ -31,6 +28,10 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import com.codahale.metrics.MetricRegistry; +import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; +import com.google.common.base.Optional; +import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,7 @@ public class JmxConnectionFactory { public JmxProxy connect(Optional handler, String host, int connectionTimeout) throws ReaperException { // use configured jmx port for host if provided - if(localMode) { + if (localMode) { host = "127.0.0.1"; } if (jmxPorts != null && jmxPorts.containsKey(host) && !host.contains(":")) { @@ -57,7 +58,7 @@ public JmxProxy connect(Optional handler, String host, int String username = null; String password = null; - if(jmxAuth != null) { + if (jmxAuth != null) { username = jmxAuth.getUsername(); password = jmxAuth.getPassword(); } @@ -68,8 +69,11 @@ public final JmxProxy connect(String host, int connectionTimeout) throws ReaperE return connect(Optional.absent(), host, connectionTimeout); } - public final JmxProxy connectAny(Optional handler, Collection hosts, int connectionTimeout) - throws ReaperException { + public final JmxProxy connectAny( + Optional handler, + Collection hosts, + int connectionTimeout) throws ReaperException { + if (hosts == null || hosts.isEmpty()) { throw new ReaperException("no hosts given for connectAny"); } @@ -77,12 +81,12 @@ public final JmxProxy connectAny(Optional handler, Collecti Collections.shuffle(hostList); Iterator hostIterator = hostList.iterator(); - for (int i=0 ; i<2 ; i++) { + for (int i = 0; i < 2; i++) { while (hostIterator.hasNext()) { String host = hostIterator.next(); // First loop, we try the most accessible nodes, then second loop we try all nodes - if(null != host && (SUCCESSFULL_CONNECTIONS.getOrDefault(host, new AtomicInteger(0)).get() >= 0 || 1 == i)) { + if (null != host && (SUCCESSFULL_CONNECTIONS.getOrDefault(host, new AtomicInteger(0)).get() >= 0 || 1 == i)) { try { JmxProxy jmxProxy = connect(handler, host, connectionTimeout); incrementSuccessfullConnections(host); @@ -98,8 +102,7 @@ public final JmxProxy connectAny(Optional handler, Collecti throw new ReaperException("no host could be reached through JMX"); } - public final JmxProxy connectAny(Cluster cluster, int connectionTimeout) - throws ReaperException { + public final JmxProxy connectAny(Cluster cluster, int connectionTimeout) throws ReaperException { Set hosts = cluster.getSeedHosts(); if (hosts == null || hosts.isEmpty()) { throw new ReaperException("no seeds in cluster with name: " + cluster.getName()); @@ -107,37 +110,39 @@ public final JmxProxy connectAny(Cluster cluster, int connectionTimeout) return connectAny(Optional.absent(), hosts, connectionTimeout); } - public void setJmxPorts(Map jmxPorts) { + public final void setJmxPorts(Map jmxPorts) { this.jmxPorts = jmxPorts; } - public void setJmxAuth(JmxCredentials jmxAuth) { + public final void setJmxAuth(JmxCredentials jmxAuth) { this.jmxAuth = jmxAuth; } - public void setAddressTranslator(EC2MultiRegionAddressTranslator addressTranslator) { + public final void setAddressTranslator(EC2MultiRegionAddressTranslator addressTranslator) { this.addressTranslator = addressTranslator; } - public void setLocalMode(boolean localMode) { + public final void setLocalMode(boolean localMode) { this.localMode = localMode; } - public void setMetricRegistry(MetricRegistry metricRegistry) { + public final void setMetricRegistry(MetricRegistry metricRegistry) { this.metricRegistry = metricRegistry; } private void incrementSuccessfullConnections(String host) { try { AtomicInteger successes = SUCCESSFULL_CONNECTIONS.putIfAbsent(host, new AtomicInteger(1)); - if(null != successes && successes.get() <= 20) { + if (null != successes && successes.get() <= 20) { successes.incrementAndGet(); } LOG.debug("Host {} has {} successfull connections", host, successes); if (null != metricRegistry) { - metricRegistry.counter(MetricRegistry.name(JmxConnectionFactory.class, "connections", host.replace('.', '-'))).inc(); + metricRegistry + .counter(MetricRegistry.name(JmxConnectionFactory.class, "connections", host.replace('.', '-'))) + .inc(); } - } catch(RuntimeException e) { + } catch (RuntimeException e) { LOG.warn("Could not increment JMX successfull connections counter for host {}", host, e); } } @@ -145,14 +150,16 @@ private void incrementSuccessfullConnections(String host) { private void decrementSuccessfullConnections(String host) { try { AtomicInteger successes = SUCCESSFULL_CONNECTIONS.putIfAbsent(host, new AtomicInteger(-1)); - if(null != successes && successes.get() >= -5) { + if (null != successes && successes.get() >= -5) { successes.decrementAndGet(); } LOG.debug("Host {} has {} successfull connections", host, successes); if (null != metricRegistry) { - metricRegistry.counter(MetricRegistry.name(JmxConnectionFactory.class, "connections", host.replace('.', '-'))).dec(); + metricRegistry + .counter(MetricRegistry.name(JmxConnectionFactory.class, "connections", host.replace('.', '-'))) + .dec(); } - } catch(RuntimeException e) { + } catch (RuntimeException e) { LOG.warn("Could not decrement JMX successfull connections counter for host {}", host, e); } } diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/JmxProxy.java b/src/server/src/main/java/com/spotify/reaper/cassandra/JmxProxy.java index 937839495..d8a220f2b 100644 --- a/src/server/src/main/java/com/spotify/reaper/cassandra/JmxProxy.java +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/JmxProxy.java @@ -11,9 +11,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.cassandra; -import static com.google.common.base.Preconditions.checkNotNull; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.service.RingRange; import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; @@ -23,7 +26,6 @@ import java.net.UnknownHostException; import java.rmi.server.RMIClientSocketFactory; import java.rmi.server.RMISocketFactory; -import java.util.AbstractMap; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -31,27 +33,32 @@ import java.util.List; import java.util.Map; import java.util.Set; -import javax.rmi.ssl.SslRMIClientSocketFactory; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; - +import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.JMX; import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.Notification; import javax.management.NotificationListener; import javax.management.ObjectName; +import javax.management.ReflectionException; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; +import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.validation.constraints.NotNull; +import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; import org.apache.cassandra.db.ColumnFamilyStoreMBean; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.CompactionManagerMBean; @@ -67,29 +74,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.datastax.driver.core.policies.EC2MultiRegionAddressTranslator; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.service.RingRange; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JmxProxy implements NotificationListener, AutoCloseable { -public class JmxProxy implements NotificationListener, AutoCloseable { + public static final Integer JMX_CONNECTION_TIMEOUT = 5; + public static final TimeUnit JMX_CONNECTION_TIMEOUT_UNIT = TimeUnit.SECONDS; private static final Logger LOG = LoggerFactory.getLogger(JmxProxy.class); private static final int JMX_PORT = 7199; private static final String JMX_URL = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; private static final String SS_OBJECT_NAME = "org.apache.cassandra.db:type=StorageService"; - private static final String AES_OBJECT_NAME = - "org.apache.cassandra.internal:type=AntiEntropySessions"; - private static final String VALIDATION_ACTIVE_OBJECT_NAME = - "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=ValidationExecutor,name=ActiveTasks"; - private static final String VALIDATION_PENDING_OBJECT_NAME = - "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=ValidationExecutor,name=PendingTasks"; - private static final String COMP_OBJECT_NAME = - "org.apache.cassandra.metrics:type=Compaction,name=PendingTasks"; + private static final String AES_OBJECT_NAME = "org.apache.cassandra.internal:type=AntiEntropySessions"; + private static final String VALIDATION_ACTIVE_OBJECT_NAME + = "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=ValidationExecutor,name=ActiveTasks"; + private static final String VALIDATION_PENDING_OBJECT_NAME + = "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=ValidationExecutor,name=PendingTasks"; + private static final String COMP_OBJECT_NAME = "org.apache.cassandra.metrics:type=Compaction,name=PendingTasks"; private static final String VALUE_ATTRIBUTE = "Value"; private static final String FAILED_TO_CONNECT_TO_USING_JMX = "Failed to connect to {} using JMX"; private static final String ERROR_GETTING_ATTR_JMX = "Error getting attribute from JMX"; @@ -109,21 +111,18 @@ public class JmxProxy implements NotificationListener, AutoCloseable { private final JMXServiceURL jmxUrl; private final String clusterName; - public static final Integer JMX_CONNECTION_TIMEOUT = 5; - public static final TimeUnit JMX_CONNECTION_TIMEOUT_UNIT = TimeUnit.SECONDS; - private JmxProxy( - Optional handler, - String host, - String hostBeforeTranslation, - JMXServiceURL jmxUrl, - JMXConnector jmxConnector, - Object ssProxy, - ObjectName ssMbeanName, - MBeanServerConnection mbeanServer, - CompactionManagerMBean cmProxy, - EndpointSnitchInfoMBean endpointSnitchMbean, - FailureDetectorMBean fdProxy) { + Optional handler, + String host, + String hostBeforeTranslation, + JMXServiceURL jmxUrl, + JMXConnector jmxConnector, + Object ssProxy, + ObjectName ssMbeanName, + MBeanServerConnection mbeanServer, + CompactionManagerMBean cmProxy, + EndpointSnitchInfoMBean endpointSnitchMbean, + FailureDetectorMBean fdProxy) { this.host = host; this.hostBeforeTranslation = hostBeforeTranslation; @@ -139,46 +138,50 @@ private JmxProxy( this.fdProxy = fdProxy; } - /** * @see JmxProxy#connect(Optional, String, int, String, String, EC2MultiRegionAddressTranslator) */ - static JmxProxy connect(Optional handler, String host, String username, - String password, final EC2MultiRegionAddressTranslator addressTranslator, int connectionTimeout) + static JmxProxy connect( + Optional handler, + String host, + String username, + String password, + final EC2MultiRegionAddressTranslator addressTranslator, + int connectionTimeout) throws ReaperException { - if(host == null) { + + if (host == null) { throw new ReaperException("Null host given to JmxProxy.connect()"); } String[] parts = host.split(":"); if (parts.length == 2) { - return connect(handler, parts[0], Integer.valueOf(parts[1]), username, password, addressTranslator, connectionTimeout); + return connect( + handler, parts[0], Integer.valueOf(parts[1]), username, password, addressTranslator, connectionTimeout); } else { return connect(handler, host, JMX_PORT, username, password, addressTranslator, connectionTimeout); } } - /** * Connect to JMX interface on the given host and port. * - * @param handler Implementation of {@link RepairStatusHandler} to process incoming - * notifications - * of repair events. - * @param host hostname or ip address of Cassandra node - * @param port port number to use for JMX connection + * @param handler Implementation of {@link RepairStatusHandler} to process incoming notifications of repair events. + * @param host hostname or ip address of Cassandra node + * @param port port number to use for JMX connection * @param username username to use for JMX authentication * @param password password to use for JMX authentication * @param addressTranslator if EC2MultiRegionAddressTranslator isn't null it will be used to translate addresses */ static JmxProxy connect( - Optional handler, - String originalHost, - int port, - String username, - String password, - final EC2MultiRegionAddressTranslator addressTranslator, - int connectionTimeout) throws ReaperException { + Optional handler, + String originalHost, + int port, + String username, + String password, + final EC2MultiRegionAddressTranslator addressTranslator, + int connectionTimeout) + throws ReaperException { ObjectName ssMbeanName; ObjectName cmMbeanName; @@ -187,7 +190,7 @@ static JmxProxy connect( JMXServiceURL jmxUrl; String host = originalHost; - if(addressTranslator != null) { + if (addressTranslator != null) { host = addressTranslator.translate(new InetSocketAddress(host, port)).getAddress().getHostAddress(); LOG.debug("translated {} to {}", originalHost, host); } @@ -209,33 +212,33 @@ static JmxProxy connect( String[] creds = {username, password}; env.put(JMXConnector.CREDENTIALS, creds); } - env.put("com.sun.jndi.rmi.factory.socket", getRMIClientSocketFactory()); + env.put("com.sun.jndi.rmi.factory.socket", getRmiClientSocketFactory()); JMXConnector jmxConn = connectWithTimeout(jmxUrl, connectionTimeout, TimeUnit.SECONDS, env); MBeanServerConnection mbeanServerConn = jmxConn.getMBeanServerConnection(); Object ssProxy = JMX.newMBeanProxy(mbeanServerConn, ssMbeanName, StorageServiceMBean.class); String cassandraVersion = ((StorageServiceMBean) ssProxy).getReleaseVersion(); - if(cassandraVersion.startsWith("2.0") || cassandraVersion.startsWith("1.")){ - ssProxy = JMX.newMBeanProxy(mbeanServerConn, ssMbeanName, StorageServiceMBean20.class); + if (cassandraVersion.startsWith("2.0") || cassandraVersion.startsWith("1.")) { + ssProxy = JMX.newMBeanProxy(mbeanServerConn, ssMbeanName, StorageServiceMBean20.class); } CompactionManagerMBean cmProxy = JMX.newMBeanProxy(mbeanServerConn, cmMbeanName, CompactionManagerMBean.class); FailureDetectorMBean fdProxy = JMX.newMBeanProxy(mbeanServerConn, fdMbeanName, FailureDetectorMBean.class); EndpointSnitchInfoMBean endpointSnitchProxy - = JMX.newMBeanProxy(mbeanServerConn, endpointSnitchMbeanName, EndpointSnitchInfoMBean.class); + = JMX.newMBeanProxy(mbeanServerConn, endpointSnitchMbeanName, EndpointSnitchInfoMBean.class); JmxProxy proxy = new JmxProxy( - handler, - host, - originalHost, - jmxUrl, - jmxConn, - ssProxy, - ssMbeanName, - mbeanServerConn, - cmProxy, - endpointSnitchProxy, - fdProxy); + handler, + host, + originalHost, + jmxUrl, + jmxConn, + ssProxy, + ssMbeanName, + mbeanServerConn, + cmProxy, + endpointSnitchProxy, + fdProxy); // registering a listener throws bunch of exceptions, so we do it here rather than in the // constructor @@ -249,12 +252,11 @@ static JmxProxy connect( } } - private static JMXConnector connectWithTimeout( - JMXServiceURL url, - long timeout, - TimeUnit unit, - Map env) throws InterruptedException, ExecutionException, TimeoutException { + JMXServiceURL url, + long timeout, + TimeUnit unit, + Map env) throws InterruptedException, ExecutionException, TimeoutException { Future future = EXECUTOR.submit(() -> JMXConnectorFactory.connect(url, env)); return future.get(timeout, unit); @@ -269,15 +271,15 @@ public String getHostBeforeTranslation() { } public String getDataCenter() { - return getDataCenter(hostBeforeTranslation); + return getDataCenter(hostBeforeTranslation); } public String getDataCenter(String host) { - try { - return endpointSnitchMbean.getDatacenter(host); - } catch (UnknownHostException ex) { - throw new IllegalArgumentException(ex); - } + try { + return endpointSnitchMbean.getDatacenter(host); + } catch (UnknownHostException ex) { + throw new IllegalArgumentException(ex); + } } /** @@ -287,14 +289,14 @@ public List getTokens() { checkNotNull(ssProxy, "Looks like the proxy is not connected"); return Lists.transform( - Lists.newArrayList(((StorageServiceMBean) ssProxy).getTokenToEndpointMap().keySet()), s -> new BigInteger(s)); + Lists.newArrayList(((StorageServiceMBean) ssProxy).getTokenToEndpointMap().keySet()), s -> new BigInteger(s)); } public Map, List> getRangeToEndpointMap(String keyspace) throws ReaperException { checkNotNull(ssProxy, "Looks like the proxy is not connected"); try { return ((StorageServiceMBean) ssProxy).getRangeToEndpointMap(keyspace); - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(e.getMessage()); throw new ReaperException(e.getMessage(), e); } @@ -308,15 +310,19 @@ public List getRangesForLocalEndpoint(String keyspace) throws ReaperE String localEndpoint = getLocalEndpoint(); // Filtering ranges for which the local node is a replica // For local mode - ranges.entrySet().stream().forEach(entry -> { - if(entry.getValue().contains(localEndpoint)) { - localRanges.add(new RingRange(new BigInteger(entry.getKey().get(0)), new BigInteger(entry.getKey().get(1)))); - } - }); + ranges + .entrySet() + .stream() + .forEach(entry -> { + if (entry.getValue().contains(localEndpoint)) { + localRanges.add( + new RingRange(new BigInteger(entry.getKey().get(0)), new BigInteger(entry.getKey().get(1)))); + } + }); LOG.info("LOCAL RANGES {}", localRanges); return localRanges; - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(e.getMessage()); throw new ReaperException(e.getMessage(), e); } @@ -326,27 +332,22 @@ public String getLocalEndpoint() { return ((StorageServiceMBean) ssProxy).getHostIdToEndpoint().get(((StorageServiceMBean) ssProxy).getLocalHostId()); } - /** * @return all hosts owning a range of tokens */ @NotNull public List tokenRangeToEndpoint(String keyspace, RingRange tokenRange) { checkNotNull(ssProxy, "Looks like the proxy is not connected"); - Set, List>> entries = ((StorageServiceMBean) ssProxy).getRangeToEndpointMap(keyspace).entrySet(); + + Set, List>> entries + = ((StorageServiceMBean) ssProxy).getRangeToEndpointMap(keyspace).entrySet(); + for (Map.Entry, List> entry : entries) { BigInteger rangeStart = new BigInteger(entry.getKey().get(0)); BigInteger rangeEnd = new BigInteger(entry.getKey().get(1)); - LOG.debug( - "[tokenRangeToEndpoint] checking token range [{}, {}) against {}", - rangeStart, - rangeEnd, - tokenRange); + LOG.debug("[tokenRangeToEndpoint] checking token range [{}, {}) against {}", rangeStart, rangeEnd, tokenRange); if (new RingRange(rangeStart, rangeEnd).encloses(tokenRange)) { - LOG.debug( - "[tokenRangeToEndpoint] Found replicas for token range {} : {}", - tokenRange, - entry.getValue()); + LOG.debug("[tokenRangeToEndpoint] Found replicas for token range {} : {}", tokenRange, entry.getValue()); return entry.getValue(); } } @@ -360,10 +361,10 @@ public List tokenRangeToEndpoint(String keyspace, RingRange tokenRange) @NotNull public Map getEndpointToHostId() { checkNotNull(ssProxy, "Looks like the proxy is not connected"); - Map hosts = Maps.newHashMap(); - try{ + Map hosts; + try { hosts = ((StorageServiceMBean) ssProxy).getEndpointToHostId(); - } catch(UndeclaredThrowableException e){ + } catch (UndeclaredThrowableException e) { hosts = ((StorageServiceMBean) ssProxy).getHostIdMap(); } return hosts; @@ -415,39 +416,39 @@ public Set getTableNamesForKeyspace(String keyspace) throws ReaperExcept /** * @return number of pending compactions on the node this proxy is connected to */ - public int getPendingCompactions() { + public int getPendingCompactions() throws MBeanException, AttributeNotFoundException, ReflectionException { checkNotNull(cmProxy, "Looks like the proxy is not connected"); try { - ObjectName name = new ObjectName(COMP_OBJECT_NAME); - int pendingCount = (int) mbeanServer.getAttribute(name, VALUE_ATTRIBUTE); - return pendingCount; - } catch (IOException ignored) { - LOG.warn(FAILED_TO_CONNECT_TO_USING_JMX, host, ignored); - } catch (MalformedObjectNameException ignored) { - LOG.error("Internal error, malformed name", ignored); - } catch (InstanceNotFoundException e) { - // This happens if no repair has yet been run on the node - // The AntiEntropySessions object is created on the first repair - LOG.error("Error getting pending compactions attribute from JMX", e); - return 0; - } catch (Exception e) { - LOG.error(ERROR_GETTING_ATTR_JMX, e); - } - // If uncertain, assume it's running + ObjectName name = new ObjectName(COMP_OBJECT_NAME); + int pendingCount = (int) mbeanServer.getAttribute(name, VALUE_ATTRIBUTE); + return pendingCount; + } catch (IOException ignored) { + LOG.warn(FAILED_TO_CONNECT_TO_USING_JMX, host, ignored); + } catch (MalformedObjectNameException ignored) { + LOG.error("Internal error, malformed name", ignored); + } catch (InstanceNotFoundException e) { + // This happens if no repair has yet been run on the node + // The AntiEntropySessions object is created on the first repair + LOG.error("Error getting pending compactions attribute from JMX", e); return 0; + } catch (RuntimeException e) { + LOG.error(ERROR_GETTING_ATTR_JMX, e); + } + // If uncertain, assume it's running + return 0; } /** * @return true if any repairs are running on the node. */ - public boolean isRepairRunning() { + public boolean isRepairRunning() throws MBeanException, AttributeNotFoundException, ReflectionException { return isRepairRunningPre22() || isRepairRunningPost22() || isValidationCompactionRunning(); } /** * @return true if any repairs are running on the node. */ - public boolean isRepairRunningPre22() { + public boolean isRepairRunningPre22() throws MBeanException, AttributeNotFoundException, ReflectionException { // Check if AntiEntropySession is actually running on the node try { ObjectName name = new ObjectName(AES_OBJECT_NAME); @@ -463,7 +464,7 @@ public boolean isRepairRunningPre22() { // The AntiEntropySessions object is created on the first repair LOG.debug("No repair has run yet on the node. Ignoring exception.", e); return false; - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(ERROR_GETTING_ATTR_JMX, e); } // If uncertain, assume it's running @@ -473,11 +474,16 @@ public boolean isRepairRunningPre22() { /** * @return true if any repairs are running on the node. */ - public boolean isValidationCompactionRunning() { + public boolean isValidationCompactionRunning() + throws MBeanException, AttributeNotFoundException, ReflectionException { + // Check if AntiEntropySession is actually running on the node try { - int activeCount = (Integer) mbeanServer.getAttribute(new ObjectName(VALIDATION_ACTIVE_OBJECT_NAME), VALUE_ATTRIBUTE); - long pendingCount = (Long) mbeanServer.getAttribute(new ObjectName(VALIDATION_PENDING_OBJECT_NAME), VALUE_ATTRIBUTE); + int activeCount + = (Integer) mbeanServer.getAttribute(new ObjectName(VALIDATION_ACTIVE_OBJECT_NAME), VALUE_ATTRIBUTE); + + long pendingCount + = (Long) mbeanServer.getAttribute(new ObjectName(VALIDATION_PENDING_OBJECT_NAME), VALUE_ATTRIBUTE); return activeCount + pendingCount != 0; } catch (IOException ignored) { @@ -487,7 +493,7 @@ public boolean isValidationCompactionRunning() { } catch (InstanceNotFoundException e) { LOG.error("Error getting pending/active validation compaction attributes from JMX", e); return false; - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(ERROR_GETTING_ATTR_JMX, e); } // If uncertain, assume it's not running @@ -504,9 +510,9 @@ public boolean isRepairRunningPost22() { // list all mbeans in search of one with the name Repair#?? // This is the replacement for AntiEntropySessions since Cassandra 2.2 Set beanSet = mbeanServer.queryNames(new ObjectName("org.apache.cassandra.internal:*"), null); - for(Object bean:beanSet) { + for (Object bean : beanSet) { ObjectName objName = (ObjectName) bean; - if(objName.getCanonicalName().contains("Repair#")){ + if (objName.getCanonicalName().contains("Repair#")) { return true; } } @@ -515,7 +521,7 @@ public boolean isRepairRunningPost22() { LOG.warn(FAILED_TO_CONNECT_TO_USING_JMX, host, ignored); } catch (MalformedObjectNameException ignored) { LOG.error("Internal error, malformed name", ignored); - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(ERROR_GETTING_ATTR_JMX, e); } // If uncertain, assume it's running @@ -528,7 +534,7 @@ public boolean isRepairRunningPost22() { public void cancelAllRepairs() { checkNotNull(ssProxy, "Looks like the proxy is not connected"); try { - ((StorageServiceMBean) ssProxy).forceTerminateAllRepairSessions(); + ((StorageServiceMBean) ssProxy).forceTerminateAllRepairSessions(); } catch (RuntimeException e) { // This can happen if the node is down (UndeclaredThrowableException), // in which case repairs will be cancelled anyway... @@ -542,8 +548,7 @@ public void cancelAllRepairs() { public boolean tableExists(String ks, String cf) { try { String type = cf.contains(".") ? "IndexColumnFamilies" : "ColumnFamilies"; - String nameStr = String.format("org.apache.cassandra.db:type=*%s,keyspace=%s,columnfamily=%s", - type, ks, cf); + String nameStr = String.format("org.apache.cassandra.db:type=*%s,keyspace=%s,columnfamily=%s", type, ks, cf); Set beans = mbeanServer.queryNames(new ObjectName(nameStr), null); if (beans.isEmpty() || beans.size() != 1) { return false; @@ -551,29 +556,34 @@ public boolean tableExists(String ks, String cf) { ObjectName bean = beans.iterator().next(); JMX.newMBeanProxy(mbeanServer, bean, ColumnFamilyStoreMBean.class); } catch (MalformedObjectNameException | IOException e) { - String errMsg = String.format("ColumnFamilyStore for %s/%s not found: %s", ks, cf, - e.getMessage()); + String errMsg = String.format("ColumnFamilyStore for %s/%s not found: %s", ks, cf, e.getMessage()); LOG.warn(errMsg, e); return false; } return true; } - public String getCassandraVersion(){ - return ((StorageServiceMBean) ssProxy).getReleaseVersion(); + public String getCassandraVersion() { + return ((StorageServiceMBean) ssProxy).getReleaseVersion(); } /** - * Triggers a repair of range (beginToken, endToken] for given keyspace and column family. - * The repair is triggered by {@link org.apache.cassandra.service.StorageServiceMBean#forceRepairRangeAsync} - * For time being, we don't allow local nor snapshot repairs. + * Triggers a repair of range (beginToken, endToken] for given keyspace and column family. The repair is triggered by + * {@link org.apache.cassandra.service.StorageServiceMBean#forceRepairRangeAsync} For time being, we don't allow local + * nor snapshot repairs. * * @return Repair command number, or 0 if nothing to repair - * @throws ReaperException */ - public int triggerRepair(BigInteger beginToken, BigInteger endToken, String keyspace, - RepairParallelism repairParallelism, Collection columnFamilies, boolean fullRepair, - Collection datacenters) throws ReaperException { + public int triggerRepair( + BigInteger beginToken, + BigInteger endToken, + String keyspace, + RepairParallelism repairParallelism, + Collection columnFamilies, + boolean fullRepair, + Collection datacenters) + throws ReaperException { + checkNotNull(ssProxy, "Looks like the proxy is not connected"); String cassandraVersion = getCassandraVersion(); boolean canUseDatacenterAware = false; @@ -582,96 +592,171 @@ public int triggerRepair(BigInteger beginToken, BigInteger endToken, String keys } catch (ReaperException e) { LOG.warn("failed on version comparison, not using dc aware repairs by default", e); } - String msg = String.format("Triggering repair of range (%s,%s] for keyspace \"%s\" on " - + "host %s, with repair parallelism %s, in cluster with Cassandra " - + "version '%s' (can use DATACENTER_AWARE '%s'), " - + "for column families: %s", - beginToken.toString(), endToken.toString(), keyspace, this.host, - repairParallelism, cassandraVersion, canUseDatacenterAware, + String msg = String.format( + "Triggering repair of range (%s,%s] for keyspace \"%s\" on " + + "host %s, with repair parallelism %s, in cluster with Cassandra " + + "version '%s' (can use DATACENTER_AWARE '%s'), " + + "for column families: %s", + beginToken.toString(), + endToken.toString(), + keyspace, + this.host, + repairParallelism, + cassandraVersion, + canUseDatacenterAware, columnFamilies); LOG.info(msg); if (repairParallelism.equals(RepairParallelism.DATACENTER_AWARE) && !canUseDatacenterAware) { - LOG.info("Cannot use DATACENTER_AWARE repair policy for Cassandra cluster with version {}," - + " falling back to SEQUENTIAL repair.", cassandraVersion); + LOG.info( + "Cannot use DATACENTER_AWARE repair policy for Cassandra cluster with version {}," + + " falling back to SEQUENTIAL repair.", + cassandraVersion); repairParallelism = RepairParallelism.SEQUENTIAL; } try { if (cassandraVersion.startsWith("2.0") || cassandraVersion.startsWith("1.")) { - return triggerRepairPre2dot1(repairParallelism, keyspace, columnFamilies, beginToken, endToken, datacenters.size() > 0 ? datacenters : null); - } else if (cassandraVersion.startsWith("2.1")){ - return triggerRepair2dot1(fullRepair, repairParallelism, keyspace, columnFamilies, beginToken, endToken, - cassandraVersion, datacenters.size() > 0 ? datacenters : null); + return triggerRepairPre2dot1( + repairParallelism, + keyspace, + columnFamilies, + beginToken, + endToken, + datacenters.size() > 0 ? datacenters : null); + } else if (cassandraVersion.startsWith("2.1")) { + return triggerRepair2dot1( + fullRepair, + repairParallelism, + keyspace, + columnFamilies, + beginToken, + endToken, + cassandraVersion, + datacenters.size() > 0 ? datacenters : null); } else { - return triggerRepairPost2dot2(fullRepair, repairParallelism, keyspace, columnFamilies, beginToken, endToken, - cassandraVersion, datacenters); + return triggerRepairPost2dot2( + fullRepair, + repairParallelism, + keyspace, + columnFamilies, + beginToken, + endToken, + cassandraVersion, + datacenters); } - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error("Segment repair failed", e); throw new ReaperException(e); } } - - public int triggerRepairPost2dot2(boolean fullRepair, RepairParallelism repairParallelism, String keyspace, - Collection columnFamilies, BigInteger beginToken, BigInteger endToken, String cassandraVersion, + public int triggerRepairPost2dot2( + boolean fullRepair, + RepairParallelism repairParallelism, + String keyspace, + Collection columnFamilies, + BigInteger beginToken, + BigInteger endToken, + String cassandraVersion, Collection datacenters) { + Map options = new HashMap<>(); options.put(RepairOption.PARALLELISM_KEY, repairParallelism.getName()); - //options.put(RepairOption.PRIMARY_RANGE_KEY, Boolean.toString(primaryRange)); + // options.put(RepairOption.PRIMARY_RANGE_KEY, Boolean.toString(primaryRange)); options.put(RepairOption.INCREMENTAL_KEY, Boolean.toString(!fullRepair)); options.put(RepairOption.JOB_THREADS_KEY, Integer.toString(1)); options.put(RepairOption.TRACE_KEY, Boolean.toString(Boolean.FALSE)); options.put(RepairOption.COLUMNFAMILIES_KEY, StringUtils.join(columnFamilies, ",")); - //options.put(RepairOption.PULL_REPAIR_KEY, Boolean.FALSE); + // options.put(RepairOption.PULL_REPAIR_KEY, Boolean.FALSE); if (fullRepair) { options.put(RepairOption.RANGES_KEY, beginToken.toString() + ":" + endToken.toString()); } options.put(RepairOption.DATACENTERS_KEY, StringUtils.join(datacenters, ",")); - //options.put(RepairOption.HOSTS_KEY, StringUtils.join(specificHosts, ",")); + // options.put(RepairOption.HOSTS_KEY, StringUtils.join(specificHosts, ",")); return ((StorageServiceMBean) ssProxy).repairAsync(keyspace, options); } - public int triggerRepair2dot1(boolean fullRepair, RepairParallelism repairParallelism, String keyspace, - Collection columnFamilies, BigInteger beginToken, BigInteger endToken, String cassandraVersion, + public int triggerRepair2dot1( + boolean fullRepair, + RepairParallelism repairParallelism, + String keyspace, + Collection columnFamilies, + BigInteger beginToken, + BigInteger endToken, + String cassandraVersion, Collection datacenters) { + if (fullRepair) { // full repair if (repairParallelism.equals(RepairParallelism.DATACENTER_AWARE)) { - return ((StorageServiceMBean) ssProxy).forceRepairRangeAsync(beginToken.toString(), endToken.toString(), - keyspace, repairParallelism.ordinal(), datacenters, - cassandraVersion.startsWith("2.2") ? new HashSet() : null, fullRepair, - columnFamilies.toArray(new String[columnFamilies.size()])); + return ((StorageServiceMBean) ssProxy) + .forceRepairRangeAsync( + beginToken.toString(), + endToken.toString(), + keyspace, + repairParallelism.ordinal(), + datacenters, + cassandraVersion.startsWith("2.2") ? new HashSet() : null, + fullRepair, + columnFamilies.toArray(new String[columnFamilies.size()])); } boolean snapshotRepair = repairParallelism.equals(RepairParallelism.SEQUENTIAL); - return ((StorageServiceMBean) ssProxy).forceRepairRangeAsync(beginToken.toString(), endToken.toString(), - keyspace, snapshotRepair ? RepairParallelism.SEQUENTIAL.ordinal() : RepairParallelism.PARALLEL.ordinal(), - datacenters, cassandraVersion.startsWith("2.2") ? new HashSet() : null, fullRepair, - columnFamilies.toArray(new String[columnFamilies.size()])); - + return ((StorageServiceMBean) ssProxy) + .forceRepairRangeAsync( + beginToken.toString(), + endToken.toString(), + keyspace, + snapshotRepair ? RepairParallelism.SEQUENTIAL.ordinal() : RepairParallelism.PARALLEL.ordinal(), + datacenters, + cassandraVersion.startsWith("2.2") ? new HashSet() : null, + fullRepair, + columnFamilies.toArray(new String[columnFamilies.size()])); } // incremental repair - return ((StorageServiceMBean) ssProxy).forceRepairAsync(keyspace, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, - fullRepair, columnFamilies.toArray(new String[columnFamilies.size()])); + return ((StorageServiceMBean) ssProxy) + .forceRepairAsync( + keyspace, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + fullRepair, + columnFamilies.toArray(new String[columnFamilies.size()])); } - public int triggerRepairPre2dot1(RepairParallelism repairParallelism, String keyspace, - Collection columnFamilies, BigInteger beginToken, BigInteger endToken, Collection datacenters) { + public int triggerRepairPre2dot1( + RepairParallelism repairParallelism, + String keyspace, + Collection columnFamilies, + BigInteger beginToken, + BigInteger endToken, + Collection datacenters) { + // Cassandra 1.2 and 2.0 compatibility if (repairParallelism.equals(RepairParallelism.DATACENTER_AWARE)) { - return ((StorageServiceMBean20) ssProxy).forceRepairRangeAsync(beginToken.toString(), endToken.toString(), - keyspace, repairParallelism.ordinal(), datacenters, null, - columnFamilies.toArray(new String[columnFamilies.size()])); + return ((StorageServiceMBean20) ssProxy) + .forceRepairRangeAsync( + beginToken.toString(), + endToken.toString(), + keyspace, + repairParallelism.ordinal(), + datacenters, + null, + columnFamilies.toArray(new String[columnFamilies.size()])); } boolean snapshotRepair = repairParallelism.equals(RepairParallelism.SEQUENTIAL); - return ((StorageServiceMBean20) ssProxy).forceRepairRangeAsync(beginToken.toString(), endToken.toString(), - keyspace, snapshotRepair, false, columnFamilies.toArray(new String[columnFamilies.size()])); - + return ((StorageServiceMBean20) ssProxy) + .forceRepairRangeAsync( + beginToken.toString(), + endToken.toString(), + keyspace, + snapshotRepair, + false, + columnFamilies.toArray(new String[columnFamilies.size()])); } public String getAllEndpointsState() { @@ -682,20 +767,19 @@ public Map getSimpleStates() { return ((FailureDetectorMBean) fdProxy).getSimpleStates(); } - /** - * Invoked when the MBean this class listens to publishes an event. - * We're only interested in repair-related events. - * Their format is explained at {@link org.apache.cassandra.service.StorageServiceMBean#forceRepairAsync} - * The format is: notification type: "repair" notification userData: int array of length 2 where - * [0] = command number [1] = ordinal of AntiEntropyService.Status + * Invoked when the MBean this class listens to publishes an event. We're only interested in repair-related events. + * Their format is explained at {@link org.apache.cassandra.service.StorageServiceMBean#forceRepairAsync} The forma + * is: notification type: "repair" notification userData: int array of length 2 where [0] = command number [1] = + * ordinal of AntiEntropyService.Status */ @Override public void handleNotification(Notification notification, Object handback) { Thread.currentThread().setName(clusterName); // we're interested in "repair" String type = notification.getType(); - LOG.debug("Received notification: {} with type {} and repairStatusHandler {}", notification, type, repairStatusHandler); + LOG.debug( + "Received notification: {} with type {} and repairStatusHandler {}", notification, type, repairStatusHandler); if (repairStatusHandler.isPresent() && ("repair").equals(type)) { processOldApiNotification(notification); } @@ -717,10 +801,9 @@ private void processOldApiNotification(Notification notification) { ActiveRepairService.Status status = ActiveRepairService.Status.values()[data[1]]; // this is some text message like "Starting repair...", "Finished repair...", etc. String message = notification.getMessage(); - // let the handler process the event + // let the handler process the even repairStatusHandler.get().handle(repairNo, Optional.of(status), Optional.absent(), message); - } catch (Exception e) { - // TODO Auto-generated catch block + } catch (RuntimeException e) { LOG.error("Error while processing JMX notification", e); } } @@ -737,10 +820,9 @@ private void processNewApiNotification(Notification notification) { ProgressEventType progress = ProgressEventType.values()[data.get("type")]; // this is some text message like "Starting repair...", "Finished repair...", etc. String message = notification.getMessage(); - // let the handler process the event + // let the handler process the even repairStatusHandler.get().handle(repairNo, Optional.absent(), Optional.of(progress), message); - } catch (Exception e) { - // TODO Auto-generated catch block + } catch (RuntimeException e) { LOG.error("Error while processing JMX notification", e); } } @@ -765,7 +847,7 @@ public boolean isConnectionAlive() { @Override public void close() throws ReaperException { LOG.debug("close JMX connection to '{}': {}", host, jmxUrl); - if(this.repairStatusHandler.isPresent()){ + if (this.repairStatusHandler.isPresent()) { try { mbeanServer.removeNotificationListener(ssMbeanName, this); LOG.debug("Successfully removed notification listener for '{}': {}", host, jmxUrl); @@ -784,17 +866,18 @@ public void close() throws ReaperException { * NOTICE: This code is loosely based on StackOverflow answer: * http://stackoverflow.com/questions/6701948/efficient-way-to-compare-version-strings-in-java * + *

* Compares two version strings. * - * Use this instead of String.compareTo() for a non-lexicographical - * comparison that works for version strings. e.g. "1.10".compareTo("1.6"). + *

+ * Use this instead of String.compareTo() for a non-lexicographical comparison that works for version strings. e.g. + * "1.10".compareTo("1.6"). * * @param str1 a string of ordinal numbers separated by decimal points. * @param str2 a string of ordinal numbers separated by decimal points. - * @return The result is a negative integer if str1 is _numerically_ less than str2. - * The result is a positive integer if str1 is _numerically_ greater than str2. - * The result is zero if the strings are _numerically_ equal. - * It does not work if "1.10" is supposed to be equal to "1.10.0". + * @return The result is a negative integer if str1 is _numerically_ less than str2. The result is a positive integer + * if str1 is _numerically_ greater than str2. The result is zero if the strings are _numerically_ equal. It does + * not work if "1.10" is supposed to be equal to "1.10.0". */ public static Integer versionCompare(String str1, String str2) throws ReaperException { try { @@ -802,37 +885,36 @@ public static Integer versionCompare(String str1, String str2) throws ReaperExce String cleanedUpStr2 = str2.split(" ")[0].replaceAll("[-_~]", "."); String[] parts1 = cleanedUpStr1.split("\\."); String[] parts2 = cleanedUpStr2.split("\\."); - int i = 0; + int idx = 0; // set index to first non-equal ordinal or length of shortest version string - while (i < parts1.length && i < parts2.length) { + while (idx < parts1.length && idx < parts2.length) { try { - Integer.parseInt(parts1[i]); - Integer.parseInt(parts2[i]); + Integer.parseInt(parts1[idx]); + Integer.parseInt(parts2[idx]); } catch (NumberFormatException ex) { - if (i == 0) { + if (idx == 0) { throw ex; // just comparing two non-version strings should fail } - // first non integer part, so let's just stop comparison here and ignore the rest - i--; + // first non integer part, so let's just stop comparison here and ignore the res + idx--; break; } - if (parts1[i].equals(parts2[i])) { - i++; + if (parts1[idx].equals(parts2[idx])) { + idx++; continue; } break; } // compare first non-equal ordinal number - if (i < parts1.length && i < parts2.length) { - int diff = Integer.valueOf(parts1[i]).compareTo(Integer.valueOf(parts2[i])); + if (idx < parts1.length && idx < parts2.length) { + int diff = Integer.valueOf(parts1[idx]).compareTo(Integer.valueOf(parts2[idx])); return Integer.signum(diff); - } - // the strings are equal or one string is a substring of the other - // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" - else { + } else { + // the strings are equal or one string is a substring of the other + // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" return Integer.signum(parts1.length - parts2.length); } - } catch (Exception ex) { + } catch (RuntimeException ex) { LOG.error("failed comparing strings for versions: '{}' '{}'", str1, str2); throw new ReaperException(ex); } @@ -840,72 +922,29 @@ public static Integer versionCompare(String str1, String str2) throws ReaperExce public void clearSnapshot(String repairId, String keyspaceName) throws ReaperException { if (repairId == null || ("").equals(repairId)) { - // Passing in null or empty string will clear all snapshots on the host + // Passing in null or empty string will clear all snapshots on the hos throw new IllegalArgumentException("repairId cannot be null or empty string"); } try { - ((StorageServiceMBean) ssProxy).clearSnapshot(repairId, keyspaceName); + ((StorageServiceMBean) ssProxy).clearSnapshot(repairId, keyspaceName); } catch (IOException e) { throw new ReaperException(e); } } - public List getLiveNodes() - throws ReaperException { + public List getLiveNodes() throws ReaperException { checkNotNull(ssProxy, "Looks like the proxy is not connected"); try { return ((StorageServiceMBean) ssProxy).getLiveNodes(); - } catch (Exception e) { + } catch (RuntimeException e) { LOG.error(e.getMessage()); throw new ReaperException(e.getMessage(), e); } } - private static RMIClientSocketFactory getRMIClientSocketFactory() { - return Boolean.parseBoolean(System.getProperty("ssl.enable")) - ? new SslRMIClientSocketFactory() - : RMISocketFactory.getDefaultSocketFactory(); - } -} - -/** - * This code is copied and adjusted from from NodeProbe.java from Cassandra source. - */ -class ColumnFamilyStoreMBeanIterator - implements Iterator> { - - private final Iterator resIter; - private final MBeanServerConnection mbeanServerConn; - - public ColumnFamilyStoreMBeanIterator(MBeanServerConnection mbeanServerConn) - throws MalformedObjectNameException, NullPointerException, IOException { - ObjectName query = new ObjectName("org.apache.cassandra.db:type=ColumnFamilies,*"); - resIter = mbeanServerConn.queryNames(query, null).iterator(); - this.mbeanServerConn = mbeanServerConn; - } - - static Iterator> getColumnFamilyStoreMBeanProxies( - MBeanServerConnection mbeanServerConn) - throws IOException, MalformedObjectNameException { - return new ColumnFamilyStoreMBeanIterator(mbeanServerConn); - } - - @Override - public boolean hasNext() { - return resIter.hasNext(); - } - - @Override - public Map.Entry next() { - ObjectName objectName = resIter.next(); - String keyspaceName = objectName.getKeyProperty("keyspace"); - ColumnFamilyStoreMBean cfsProxy = - JMX.newMBeanProxy(mbeanServerConn, objectName, ColumnFamilyStoreMBean.class); - return new AbstractMap.SimpleImmutableEntry<>(keyspaceName, cfsProxy); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); + private static RMIClientSocketFactory getRmiClientSocketFactory() { + return Boolean.parseBoolean(System.getProperty("ssl.enable")) + ? new SslRMIClientSocketFactory() + : RMISocketFactory.getDefaultSocketFactory(); } } diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/RepairStatusHandler.java b/src/server/src/main/java/com/spotify/reaper/cassandra/RepairStatusHandler.java index d8d73b21e..b88d2545c 100644 --- a/src/server/src/main/java/com/spotify/reaper/cassandra/RepairStatusHandler.java +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/RepairStatusHandler.java @@ -11,27 +11,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.cassandra; +import com.google.common.base.Optional; import org.apache.cassandra.service.ActiveRepairService; -import org.apache.cassandra.streaming.StreamEvent; import org.apache.cassandra.utils.progress.ProgressEventType; -import com.google.common.base.Optional; public interface RepairStatusHandler { /** * Handle an event representing a change in the state of a running repair. * - * Implementation of this method is intended to persist the repair state change in Reaper's - * state. + *

+ * Implementation of this method is intended to persist the repair state change in Reaper's state. * * @param repairNumber repair sequence number, obtained when triggering a repair - * @param status new status of the repair (old API) - * @param progress new status of the repair (new API) - * @param message additional information about the repair + * @param status new status of the repair (old API) + * @param progress new status of the repair (new API) + * @param message additional information about the repair */ - void handle(int repairNumber, Optional status, Optional progress, String message); - + void handle( + int repairNumber, + Optional status, + Optional progress, + String message); } diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/StorageServiceMBean20.java b/src/server/src/main/java/com/spotify/reaper/cassandra/StorageServiceMBean20.java index 6f56a0125..bcbb45bd5 100644 --- a/src/server/src/main/java/com/spotify/reaper/cassandra/StorageServiceMBean20.java +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/StorageServiceMBean20.java @@ -1,14 +1,22 @@ +/* + * 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.spotify.reaper.cassandra; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.util.Collection; -import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import javax.management.NotificationEmitter; @@ -16,479 +24,134 @@ public interface StorageServiceMBean20 extends NotificationEmitter, StorageServiceMBean { - /** - * Retrieve the list of live nodes in the cluster, where "liveness" is - * determined by the failure detector of the node being queried. - * - * @return set of IP addresses, as Strings - */ - public List getLiveNodes(); - - /** - * Retrieve the list of unreachable nodes in the cluster, as determined - * by this node's failure detector. - * - * @return set of IP addresses, as Strings - */ - public List getUnreachableNodes(); - - /** - * Retrieve the list of nodes currently bootstrapping into the ring. - * - * @return set of IP addresses, as Strings - */ - public List getJoiningNodes(); - - /** - * Retrieve the list of nodes currently leaving the ring. - * - * @return set of IP addresses, as Strings - */ - public List getLeavingNodes(); - - /** - * Retrieve the list of nodes currently moving in the ring. - * - * @return set of IP addresses, as Strings - */ - public List getMovingNodes(); - - /** - * Fetch string representations of the tokens for this node. - * - * @return a collection of tokens formatted as strings - */ - public List getTokens(); - - /** - * Fetch string representations of the tokens for a specified node. - * - * @param endpoint string representation of an node - * @return a collection of tokens formatted as strings - */ - public List getTokens(String endpoint) throws UnknownHostException; - - /** - * Fetch a string representation of the Cassandra version. - * @return A string representation of the Cassandra version. - */ - public String getReleaseVersion(); - - /** - * Fetch a string representation of the current Schema version. - * @return A string representation of the Schema version. - */ - public String getSchemaVersion(); - - - /** - * Get the list of all data file locations from conf - * @return String array of all locations - */ - public String[] getAllDataFileLocations(); - - /** - * Get location of the commit log - * @return a string path - */ - public String getCommitLogLocation(); - - /** - * Get location of the saved caches dir - * @return a string path - */ - public String getSavedCachesLocation(); - - /** - * Retrieve a map of range to end points that describe the ring topology - * of a Cassandra cluster. - * - * @return mapping of ranges to end points - */ - public Map, List> getRangeToEndpointMap(String keyspace); - - /** - * Retrieve a map of range to rpc addresses that describe the ring topology - * of a Cassandra cluster. - * - * @return mapping of ranges to rpc addresses - */ - public Map, List> getRangeToRpcaddressMap(String keyspace); - - /** - * The same as {@code describeRing(String)} but converts TokenRange to the String for JMX compatibility - * - * @param keyspace The keyspace to fetch information about - * - * @return a List of TokenRange(s) converted to String for the given keyspace - */ - public List describeRingJMX(String keyspace) throws IOException; - - /** - * Retrieve a map of pending ranges to endpoints that describe the ring topology - * @param keyspace the keyspace to get the pending range map for. - * @return a map of pending ranges to endpoints - */ - public Map, List> getPendingRangeToEndpointMap(String keyspace); - - /** - * Retrieve a map of tokens to endpoints, including the bootstrapping - * ones. - * - * @return a map of tokens to endpoints in ascending order - */ - public Map getTokenToEndpointMap(); - - /** Retrieve this hosts unique ID */ - public String getLocalHostId(); - - /** Retrieve the mapping of endpoint to host ID */ - public Map getHostIdMap(); - - /** - * Numeric load value. - * @see org.apache.cassandra.metrics.StorageMetrics#load - */ - @Deprecated - public double getLoad(); - - /** Human-readable load value */ - public String getLoadString(); - - /** Human-readable load value. Keys are IP addresses. */ - public Map getLoadMap(); - - /** - * Return the generation value for this node. - * - * @return generation number - */ - public int getCurrentGenerationNumber(); - - /** - * This method returns the N endpoints that are responsible for storing the - * specified key i.e for replication. - * - * @param keyspaceName keyspace name - * @param cf Column family name - * @param key - key for which we need to find the endpoint return value - - * the endpoint responsible for this key - */ - public List getNaturalEndpoints(String keyspaceName, String cf, String key); - public List getNaturalEndpoints(String keyspaceName, ByteBuffer key); - - /** - * Takes the snapshot for the given keyspaces. A snapshot name must be specified. - * - * @param tag the tag given to the snapshot; may not be null or empty - * @param keyspaceNames the name of the keyspaces to snapshot; empty means "all." - */ - public void takeSnapshot(String tag, String... keyspaceNames) throws IOException; - - /** - * Takes the snapshot of a specific column family. A snapshot name must be specified. - * - * @param keyspaceName the keyspace which holds the specified column family - * @param columnFamilyName the column family to snapshot - * @param tag the tag given to the snapshot; may not be null or empty - */ - public void takeColumnFamilySnapshot(String keyspaceName, String columnFamilyName, String tag) throws IOException; - - /** - * Remove the snapshot with the given name from the given keyspaces. - * If no tag is specified we will remove all snapshots. - */ - public void clearSnapshot(String tag, String... keyspaceNames) throws IOException; - - /** - * Forces major compaction of a single keyspace - */ - public void forceKeyspaceCompaction(String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException; - - public void forceKeyspaceFlush(String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException; - - /** - * Invoke repair asynchronously. - * You can track repair progress by subscribing JMX notification sent from this StorageServiceMBean. - * Notification format is: - * type: "repair" - * userObject: int array of length 2, [0]=command number, [1]=ordinal of AntiEntropyService.Status - * - * @return Repair command number, or 0 if nothing to repair - */ - public int forceRepairAsync(String keyspace, boolean isSequential, Collection dataCenters, final Collection hosts, boolean primaryRange, String... columnFamilies); - - /** - * Invoke repair asynchronously. - * You can track repair progress by subscribing JMX notification sent from this StorageServiceMBean. - * Notification format is: - * type: "repair" - * userObject: int array of length 2, [0]=command number, [1]=ordinal of AntiEntropyService.Status - * - * @param parallelismDegree 0: sequential, 1: parallel, 2: DC parallel - * @return Repair command number, or 0 if nothing to repair - */ - public int forceRepairAsync(String keyspace, int parallelismDegree, Collection dataCenters, final Collection hosts, boolean primaryRange, String... columnFamilies); - - /** - * Same as forceRepairAsync, but handles a specified range - */ - public int forceRepairRangeAsync(String beginToken, String endToken, final String keyspaceName, boolean isSequential, Collection dataCenters, final Collection hosts, final String... columnFamilies); - - /** - * Same as forceRepairAsync, but handles a specified range - * - * @param parallelismDegree 0: sequential, 1: parallel, 2: DC parallel - */ - public int forceRepairRangeAsync(String beginToken, String endToken, final String keyspaceName, int parallelismDegree, Collection dataCenters, final Collection hosts, final String... columnFamilies); - - /** - * Invoke repair asynchronously. - * You can track repair progress by subscribing JMX notification sent from this StorageServiceMBean. - * Notification format is: - * type: "repair" - * userObject: int array of length 2, [0]=command number, [1]=ordinal of AntiEntropyService.Status - * - * @return Repair command number, or 0 if nothing to repair - * @see #forceKeyspaceRepair(String, boolean, boolean, String...) - */ - public int forceRepairAsync(String keyspace, boolean isSequential, boolean isLocal, boolean primaryRange, String... columnFamilies); - - /** - * Same as forceRepairAsync, but handles a specified range - */ - public int forceRepairRangeAsync(String beginToken, String endToken, final String keyspaceName, boolean isSequential, boolean isLocal, final String... columnFamilies); - - /** - * Triggers proactive repair for given column families, or all columnfamilies for the given keyspace - * if none are explicitly listed. - * @param keyspaceName - * @param columnFamilies - * @throws IOException - */ - public void forceKeyspaceRepair(String keyspaceName, boolean isSequential, boolean isLocal, String... columnFamilies) throws IOException; - - /** - * Triggers proactive repair but only for the node primary range. - */ - public void forceKeyspaceRepairPrimaryRange(String keyspaceName, boolean isSequential, boolean isLocal, String... columnFamilies) throws IOException; - - /** - * Perform repair of a specific range. - * - * This allows incremental repair to be performed by having an external controller submitting repair jobs. - * Note that the provided range much be a subset of one of the node local range. - */ - public void forceKeyspaceRepairRange(String beginToken, String endToken, String keyspaceName, boolean isSequential, boolean isLocal, String... columnFamilies) throws IOException; - - public void forceTerminateAllRepairSessions(); - - /** - * transfer this node's data to other machines and remove it from service. - */ - public void decommission() throws InterruptedException; - - /** - * @param newToken token to move this node to. - * This node will unload its data onto its neighbors, and bootstrap to the new token. - */ - public void move(String newToken) throws IOException; - - /** - * removeToken removes token (and all data associated with - * enpoint that had it) from the ring - */ - public void removeNode(String token); - - /** - * Get the status of a token removal. - */ - public String getRemovalStatus(); - - /** - * Force a remove operation to finish. - */ - public void forceRemoveCompletion(); - - /** set the logging level at runtime */ - public void setLog4jLevel(String classQualifier, String level); - - public MapgetLoggingLevels(); - - /** get the operational mode (leaving, joining, normal, decommissioned, client) **/ - public String getOperationMode(); - - /** Returns whether the storage service is starting or not */ - public boolean isStarting(); - - /** get the progress of a drain operation */ - public String getDrainProgress(); - - /** makes node unavailable for writes, flushes memtables and replays commitlog. */ - public void drain() throws IOException, InterruptedException, ExecutionException; - - /** - * Truncates (deletes) the given columnFamily from the provided keyspace. - * Calling truncate results in actual deletion of all data in the cluster - * under the given columnFamily and it will fail unless all hosts are up. - * All data in the given column family will be deleted, but its definition - * will not be affected. - * - * @param keyspace The keyspace to delete from - * @param columnFamily The column family to delete data from. - */ - public void truncate(String keyspace, String columnFamily)throws TimeoutException, IOException; - - /** - * given a list of tokens (representing the nodes in the cluster), returns - * a mapping from "token -> %age of cluster owned by that token" - */ - public Map getOwnership(); - - /** - * Effective ownership is % of the data each node owns given the keyspace - * we calculate the percentage using replication factor. - * If Keyspace == null, this method will try to verify if all the keyspaces - * in the cluster have the same replication strategies and if yes then we will - * use the first else a empty Map is returned. - */ - public Map effectiveOwnership(String keyspace) throws IllegalStateException; - - public List getKeyspaces(); - - /** - * Change endpointsnitch class and dynamic-ness (and dynamic attributes) at runtime - * @param epSnitchClassName the canonical path name for a class implementing IEndpointSnitch - * @param dynamic boolean that decides whether dynamicsnitch is used or not - * @param dynamicUpdateInterval integer, in ms (default 100) - * @param dynamicResetInterval integer, in ms (default 600,000) - * @param dynamicBadnessThreshold double, (default 0.0) - */ - public void updateSnitch(String epSnitchClassName, Boolean dynamic, Integer dynamicUpdateInterval, Integer dynamicResetInterval, Double dynamicBadnessThreshold) throws ClassNotFoundException; - - // allows a user to forcibly 'kill' a sick node - public void stopGossiping(); - - // allows a user to recover a forcibly 'killed' node - public void startGossiping(); - - // allows a user to see whether gossip is running or not - public boolean isGossipRunning(); - - // allows a user to forcibly completely stop cassandra - public void stopDaemon(); - - // to determine if gossip is disabled - public boolean isInitialized(); - - // allows a user to disable thrift - public void stopRPCServer(); - - // allows a user to reenable thrift - public void startRPCServer(); - - // to determine if thrift is running - public boolean isRPCServerRunning(); - - public void stopNativeTransport(); - public void startNativeTransport(); - public boolean isNativeTransportRunning(); - - // allows a node that have been started without joining the ring to join it - public void joinRing() throws IOException; - public boolean isJoined(); - - @Deprecated - public int getExceptionCount(); - - public void setStreamThroughputMbPerSec(int value); - public int getStreamThroughputMbPerSec(); - - public int getCompactionThroughputMbPerSec(); - public void setCompactionThroughputMbPerSec(int value); - - public boolean isIncrementalBackupsEnabled(); - public void setIncrementalBackupsEnabled(boolean value); - - /** - * Initiate a process of streaming data for which we are responsible from other nodes. It is similar to bootstrap - * except meant to be used on a node which is already in the cluster (typically containing no data) as an - * alternative to running repair. - * - * @param sourceDc Name of DC from which to select sources for streaming or null to pick any node - */ - public void rebuild(String sourceDc); - - /** Starts a bulk load and blocks until it completes. */ - public void bulkLoad(String directory); - - /** - * Starts a bulk load asynchronously and returns the String representation of the planID for the new - * streaming session. - */ - public String bulkLoadAsync(String directory); - - public void rescheduleFailedDeletions(); - - /** - * Load new SSTables to the given keyspace/columnFamily - * - * @param ksName The parent keyspace name - * @param cfName The ColumnFamily name where SSTables belong - */ - public void loadNewSSTables(String ksName, String cfName); - - /** - * Return a List of Tokens representing a sample of keys across all ColumnFamilyStores. - * - * Note: this should be left as an operation, not an attribute (methods starting with "get") - * to avoid sending potentially multiple MB of data when accessing this mbean by default. See CASSANDRA-4452. - * - * @return set of Tokens as Strings - */ - public List sampleKeyRange(); - - /** - * rebuild the specified indexes - */ - public void rebuildSecondaryIndex(String ksName, String cfName, String... idxNames); - - public void resetLocalSchema() throws IOException; - - /** - * Enables/Disables tracing for the whole system. Only thrift requests can start tracing currently. - * - * @param probability - * ]0,1[ will enable tracing on a partial number of requests with the provided probability. 0 will - * disable tracing and 1 will enable tracing for all requests (which mich severely cripple the system) - */ - public void setTraceProbability(double probability); - - /** - * Returns the configured tracing probability. - */ - public double getTracingProbability(); - - void disableAutoCompaction(String ks, String ... columnFamilies) throws IOException; - void enableAutoCompaction(String ks, String ... columnFamilies) throws IOException; - - public void deliverHints(String host) throws UnknownHostException; - - /** Returns the name of the cluster */ - public String getClusterName(); - /** Returns the cluster partitioner */ - public String getPartitionerName(); - - /** Returns the threshold for warning of queries with many tombstones */ - public int getTombstoneWarnThreshold(); - /** Sets the threshold for warning queries with many tombstones */ - public void setTombstoneWarnThreshold(int tombstoneDebugThreshold); - - /** Returns the threshold for abandoning queries with many tombstones */ - public int getTombstoneFailureThreshold(); - /** Sets the threshold for abandoning queries with many tombstones */ - public void setTombstoneFailureThreshold(int tombstoneDebugThreshold); + /** + * Numeric load value. + * + * @see org.apache.cassandra.metrics.StorageMetrics#load + */ + @Deprecated + double getLoad(); + + /** + * Forces major compaction of a single keyspace + */ + void forceKeyspaceCompaction(String keyspaceName, String... columnFamilies) + throws IOException, ExecutionException, InterruptedException; + + /** + * Invoke repair asynchronously. You can track repair progress by subscribing JMX notification sent from this + * StorageServiceMBean. Notification format is: type: "repair" userObject: int array of length 2, [0]=command number, + * [1]=ordinal of AntiEntropyService.Status + * + * @return Repair command number, or 0 if nothing to repair + */ + int forceRepairAsync( + String keyspace, + boolean isSequential, + Collection dataCenters, + final Collection hosts, + boolean primaryRange, + String... columnFamilies); + + /** + * Invoke repair asynchronously. You can track repair progress by subscribing JMX notification sent from this + * StorageServiceMBean. Notification format is: type: "repair" userObject: int array of length 2, [0]=command number, + * [1]=ordinal of AntiEntropyService.Status + * + * @param parallelismDegree 0: sequential, 1: parallel, 2: DC parallel + * @return Repair command number, or 0 if nothing to repair + */ + int forceRepairAsync( + String keyspace, + int parallelismDegree, + Collection dataCenters, + final Collection hosts, + boolean primaryRange, + String... columnFamilies); + + /** + * Invoke repair asynchronously. You can track repair progress by subscribing JMX notification sent from this + * StorageServiceMBean. Notification format is: type: "repair" userObject: int array of length 2, [0]=command number, + * [1]=ordinal of AntiEntropyService.Status + * + * @return Repair command number, or 0 if nothing to repair + * @see #forceKeyspaceRepair(String, boolean, boolean, String...) + */ + int forceRepairAsync( + String keyspace, boolean isSequential, boolean isLocal, boolean primaryRange, String... columnFamilies); + + /** + * Same as forceRepairAsync, but handles a specified range + */ + int forceRepairRangeAsync( + String beginToken, + String endToken, + final String keyspaceName, + boolean isSequential, + Collection dataCenters, + final Collection hosts, + final String... columnFamilies); + + /** + * Same as forceRepairAsync, but handles a specified range + * + * @param parallelismDegree 0: sequential, 1: parallel, 2: DC parallel + */ + int forceRepairRangeAsync( + String beginToken, + String endToken, + final String keyspaceName, + int parallelismDegree, + Collection dataCenters, + final Collection hosts, + final String... columnFamilies); + + /** + * Same as forceRepairAsync, but handles a specified range + */ + int forceRepairRangeAsync( + String beginToken, + String endToken, + final String keyspaceName, + boolean isSequential, + boolean isLocal, + final String... columnFamilies); + + /** + * Triggers proactive repair for given column families, or all columnfamilies for the given keyspace if none are + * explicitly listed. + */ + void forceKeyspaceRepair(String keyspaceName, boolean isSequential, boolean isLocal, String... columnFamilies) + throws IOException; + + /** + * Triggers proactive repair but only for the node primary range. + */ + void forceKeyspaceRepairPrimaryRange( + String keyspaceName, boolean isSequential, boolean isLocal, String... columnFamilies) throws IOException; + + /** + * Perform repair of a specific range. + * + *

+ * This allows incremental repair to be performed by having an external controller submitting repair jobs. Note + * that the provided range much be a subset of one of the node local range. + */ + void forceKeyspaceRepairRange( + String beginToken, + String endToken, + String keyspaceName, + boolean isSequential, + boolean isLocal, + String... columnFamilies) + throws IOException; + + /** + * set the logging level at runtime + */ + void setLog4jLevel(String classQualifier, String level); + + @Deprecated + int getExceptionCount(); - /** Sets the hinted handoff throttle in kb per second, per delivery thread. */ - public void setHintedHandoffThrottleInKB(int throttleInKB); } diff --git a/src/server/src/main/java/com/spotify/reaper/cassandra/package-info.java b/src/server/src/main/java/com/spotify/reaper/cassandra/package-info.java new file mode 100644 index 000000000..bc10cfecf --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/cassandra/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.cassandra; diff --git a/src/server/src/main/java/com/spotify/reaper/core/Cluster.java b/src/server/src/main/java/com/spotify/reaper/core/Cluster.java index cf958a81d..97096eb5b 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/Cluster.java +++ b/src/server/src/main/java/com/spotify/reaper/core/Cluster.java @@ -11,27 +11,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.core; import java.util.Set; -public class Cluster { +import com.google.common.base.Preconditions; + +public final class Cluster { private final String name; private final String partitioner; // Full name of the partitioner class private final Set seedHosts; - public static String toSymbolicName(String s) { - assert s != null : "cannot turn null into symbolic name"; - return s.toLowerCase().replaceAll("[^a-z0-9_\\-\\.]", ""); - } - public Cluster(String name, String partitioner, Set seedHosts) { this.name = toSymbolicName(name); this.partitioner = partitioner; this.seedHosts = seedHosts; } + public static String toSymbolicName(String name) { + Preconditions.checkNotNull(name, "cannot turn null into symbolic name"); + return name.toLowerCase().replaceAll("[^a-z0-9_\\-\\.]", ""); + } + public String getName() { return name; } diff --git a/src/server/src/main/java/com/spotify/reaper/core/NodeMetrics.java b/src/server/src/main/java/com/spotify/reaper/core/NodeMetrics.java index 96417096d..40a6901bf 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/NodeMetrics.java +++ b/src/server/src/main/java/com/spotify/reaper/core/NodeMetrics.java @@ -1,13 +1,27 @@ +/* + * 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.spotify.reaper.core; public final class NodeMetrics { + private final String hostAddress; private final String datacenter; private final int pendingCompactions; private final boolean hasRepairRunning; private final int activeAnticompactions; - private NodeMetrics(Builder builder) { this.hostAddress = builder.hostAddress; this.datacenter = builder.datacenter; @@ -36,55 +50,56 @@ public int getActiveAnticompactions() { return activeAnticompactions; } - /** * Creates builder to build {@link NodeMetrics}. + * * @return created builder */ public static Builder builder() { - return new Builder(); + return new Builder(); } /** * Builder to build {@link NodeMetrics}. */ public static final class Builder { - private String hostAddress; - private String datacenter; - private int pendingCompactions; - private boolean hasRepairRunning; - private int activeAnticompactions; - private Builder() { - } + private String hostAddress; + private String datacenter; + private int pendingCompactions; + private boolean hasRepairRunning; + private int activeAnticompactions; - public Builder withHostAddress(String hostAddress) { - this.hostAddress = hostAddress; - return this; - } + private Builder() { + } + + public Builder withHostAddress(String hostAddress) { + this.hostAddress = hostAddress; + return this; + } - public Builder withDatacenter(String datacenter) { + public Builder withDatacenter(String datacenter) { this.datacenter = datacenter; return this; } - public Builder withPendingCompactions(int pendingCompactions) { - this.pendingCompactions = pendingCompactions; - return this; - } + public Builder withPendingCompactions(int pendingCompactions) { + this.pendingCompactions = pendingCompactions; + return this; + } - public Builder withHasRepairRunning(boolean hasRepairRunning) { - this.hasRepairRunning = hasRepairRunning; - return this; - } + public Builder withHasRepairRunning(boolean hasRepairRunning) { + this.hasRepairRunning = hasRepairRunning; + return this; + } - public Builder withActiveAnticompactions(int activeAnticompactions) { - this.activeAnticompactions = activeAnticompactions; - return this; - } + public Builder withActiveAnticompactions(int activeAnticompactions) { + this.activeAnticompactions = activeAnticompactions; + return this; + } - public NodeMetrics build() { - return new NodeMetrics(this); - } + public NodeMetrics build() { + return new NodeMetrics(this); + } } } diff --git a/src/server/src/main/java/com/spotify/reaper/core/RepairRun.java b/src/server/src/main/java/com/spotify/reaper/core/RepairRun.java index 6ab04b3b1..207563af2 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/RepairRun.java +++ b/src/server/src/main/java/com/spotify/reaper/core/RepairRun.java @@ -11,6 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.core; import java.util.Objects; @@ -20,14 +21,13 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; -public class RepairRun implements Comparable { +public final class RepairRun implements Comparable { private final UUID id; // IDEA: maybe we want to have start and stop token for parallel runners on same repair run? - //private final long startToken; - //private final long endToken; - + // private final long startToken; + // private final long endToken; private final String cause; private final String owner; private final String clusterName; @@ -102,7 +102,7 @@ public DateTime getPauseTime() { public double getIntensity() { return intensity; } - + public String getLastEvent() { return lastEvent; } @@ -120,11 +120,10 @@ public Builder with() { } /** - * Order RepairRun instances by time. Primarily endTime, secondarily startTime. Descending, i.e. - * latest first. + * Order RepairRun instances by time. Primarily endTime, secondarily startTime. Descending, i.e. latest first. + * * @param other the RepairRun compared to - * @return negative if this RepairRun is later than the specified RepairRun. Positive if earlier. - * 0 if equal. + * @return negative if this RepairRun is later than the specified RepairRun. Positive if earlier. 0 if equal. */ @Override public int compareTo(RepairRun other) { @@ -136,19 +135,19 @@ public int compareTo(RepairRun other) { return -comparator.compare(startTime, other.startTime); } } - + @Override public boolean equals(Object other) { - if (other == this) + if (other == this) { return true; + } if (!(other instanceof RepairRun)) { return false; } RepairRun run = (RepairRun) other; - return this.id == run.id - && this.repairUnitId == run.repairUnitId; + return this.id == run.id && this.repairUnitId == run.repairUnitId; } - + @Override public int hashCode() { return Objects.hash(this.id, this.repairUnitId); @@ -172,14 +171,13 @@ public boolean isTerminated() { } } - public static class Builder { + public static final class Builder { public final String clusterName; public final UUID repairUnitId; private RunState runState; private DateTime creationTime; private double intensity; - private boolean incrementalRepair; private String cause; private String owner; private DateTime startTime; @@ -189,8 +187,14 @@ public static class Builder { private int segmentCount; private RepairParallelism repairParallelism; - public Builder(String clusterName, UUID repairUnitId, DateTime creationTime, - double intensity, int segmentCount, RepairParallelism repairParallelism) { + public Builder( + String clusterName, + UUID repairUnitId, + DateTime creationTime, + double intensity, + int segmentCount, + RepairParallelism repairParallelism) { + this.clusterName = clusterName; this.repairUnitId = repairUnitId; this.runState = RunState.NOT_STARTED; @@ -230,7 +234,7 @@ public Builder intensity(double intensity) { this.intensity = intensity; return this; } - + public Builder cause(String cause) { this.cause = cause; return this; diff --git a/src/server/src/main/java/com/spotify/reaper/core/RepairSchedule.java b/src/server/src/main/java/com/spotify/reaper/core/RepairSchedule.java index 27eae2248..2faba5660 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/RepairSchedule.java +++ b/src/server/src/main/java/com/spotify/reaper/core/RepairSchedule.java @@ -11,20 +11,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.core; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import java.util.Collection; - import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; -public class RepairSchedule { +public final class RepairSchedule { private final UUID id; @@ -84,15 +84,11 @@ public ImmutableList getRunHistory() { } /** - * Required for JDBI mapping into database. - * Generic collection type would be hard to map into Postgres array types. + * Required for JDBI mapping into database. Generic collection type would be hard to map into Postgres array types. */ - public LongCollectionSQLType getRunHistorySQL() { - List list = runHistory - .stream() - .map(UUID::getMostSignificantBits) - .collect(Collectors.toList()); - return new LongCollectionSQLType(list); + public LongCollectionSqlType getRunHistorySql() { + List list = runHistory.stream().map(UUID::getMostSignificantBits).collect(Collectors.toList()); + return new LongCollectionSqlType(list); } public int getSegmentCount() { @@ -143,10 +139,16 @@ public static class Builder { private String owner; private DateTime pauseTime; - public Builder(UUID repairUnitId, State state, int daysBetween, DateTime nextActivation, - ImmutableList runHistory, int segmentCount, - RepairParallelism repairParallelism, - double intensity, DateTime creationTime) { + public Builder( + UUID repairUnitId, + State state, + int daysBetween, + DateTime nextActivation, + ImmutableList runHistory, + int segmentCount, + RepairParallelism repairParallelism, + double intensity, + DateTime creationTime) { this.repairUnitId = repairUnitId; this.state = state; this.daysBetween = daysBetween; @@ -173,8 +175,7 @@ private Builder(RepairSchedule original) { intensity = original.intensity; } - - public Builder state(State state) { + public Builder state(State state) { this.state = state; return this; } @@ -229,19 +230,19 @@ public RepairSchedule build(UUID id) { } } - /** - * This is required to be able to map in generic manner into Postgres array types through JDBI. - */ - public static final class LongCollectionSQLType { + /** + * This is required to be able to map in generic manner into Postgres array types through JDBI. + */ + public static final class LongCollectionSqlType { - private final Collection collection; + private final Collection collection; - public LongCollectionSQLType(Collection collection) { - this.collection = collection; - } + public LongCollectionSqlType(Collection collection) { + this.collection = collection; + } - public Collection getValue() { - return null == collection ? collection : Lists.newArrayList(); - } + public Collection getValue() { + return null == collection ? collection : Lists.newArrayList(); } + } } diff --git a/src/server/src/main/java/com/spotify/reaper/core/RepairSegment.java b/src/server/src/main/java/com/spotify/reaper/core/RepairSegment.java index 2188af594..09bc54286 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/RepairSegment.java +++ b/src/server/src/main/java/com/spotify/reaper/core/RepairSegment.java @@ -11,16 +11,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.core; import com.spotify.reaper.service.RingRange; -import org.joda.time.DateTime; - import java.math.BigInteger; import java.util.UUID; -public class RepairSegment { +import org.joda.time.DateTime; + +public final class RepairSegment { private final UUID id; private final UUID runId; @@ -106,7 +107,6 @@ public enum State { public static class Builder { - public final RingRange tokenRange; private final UUID repairUnitId; private UUID runId; @@ -136,9 +136,9 @@ private Builder(RepairSegment original) { endTime = original.endTime; } - public Builder withRunId(UUID runId){ - this.runId = runId; - return this; + public Builder withRunId(UUID runId) { + this.runId = runId; + return this; } public Builder failCount(int failCount) { diff --git a/src/server/src/main/java/com/spotify/reaper/core/RepairUnit.java b/src/server/src/main/java/com/spotify/reaper/core/RepairUnit.java index b121f27d6..fe4985c96 100644 --- a/src/server/src/main/java/com/spotify/reaper/core/RepairUnit.java +++ b/src/server/src/main/java/com/spotify/reaper/core/RepairUnit.java @@ -11,12 +11,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.core; import java.util.Set; import java.util.UUID; -public class RepairUnit { +public final class RepairUnit { private final UUID id; private final String clusterName; @@ -77,8 +78,13 @@ public static class Builder { public final Set nodes; public final Set datacenters; - public Builder(String clusterName, String keyspaceName, Set columnFamilies, Boolean incrementalRepair, - Set nodes, Set datacenters) { + public Builder( + String clusterName, + String keyspaceName, + Set columnFamilies, + Boolean incrementalRepair, + Set nodes, + Set datacenters) { this.clusterName = clusterName; this.keyspaceName = keyspaceName; this.columnFamilies = columnFamilies; diff --git a/src/server/src/main/java/com/spotify/reaper/core/package-info.java b/src/server/src/main/java/com/spotify/reaper/core/package-info.java new file mode 100644 index 000000000..c69bc07f8 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/core/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.core; diff --git a/src/server/src/main/java/com/spotify/reaper/package-info.java b/src/server/src/main/java/com/spotify/reaper/package-info.java new file mode 100644 index 000000000..55f643239 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper; diff --git a/src/server/src/main/java/com/spotify/reaper/resources/ClusterResource.java b/src/server/src/main/java/com/spotify/reaper/resources/ClusterResource.java index 49bf265b9..0cf895023 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/ClusterResource.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/ClusterResource.java @@ -11,9 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources; -import com.google.common.base.Optional; import com.spotify.reaper.AppContext; import com.spotify.reaper.ReaperException; @@ -23,15 +23,11 @@ import com.spotify.reaper.resources.view.NodesStatus; import com.spotify.reaper.resources.view.RepairRunStatus; import com.spotify.reaper.resources.view.RepairScheduleStatus; - import com.spotify.reaper.service.ClusterRepairScheduler; -import jersey.repackaged.com.google.common.collect.Lists; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -47,7 +43,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; - import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -61,14 +56,19 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import com.google.common.base.Optional; +import jersey.repackaged.com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Path("/cluster") @Produces(MediaType.APPLICATION_JSON) -public class ClusterResource { +public final class ClusterResource { private static final int JMX_NODE_STATUS_CONCURRENCY = 3; private static final ExecutorService CLUSTER_STATUS_EXECUTOR - = Executors.newFixedThreadPool(JMX_NODE_STATUS_CONCURRENCY *2); + = Executors.newFixedThreadPool(JMX_NODE_STATUS_CONCURRENCY * 2); private static final Logger LOG = LoggerFactory.getLogger(ClusterResource.class); @@ -81,7 +81,9 @@ public ClusterResource(AppContext context) { } @GET - public Response getClusterList(@QueryParam("seedHost") Optional seedHost) { + public Response getClusterList( + @QueryParam("seedHost") Optional seedHost) { + LOG.debug("get cluster list called"); Collection clusters = context.storage.getClusters(); List clusterNames = new ArrayList<>(); @@ -101,29 +103,32 @@ public Response getClusterList(@QueryParam("seedHost") Optional seedHost @Path("/{cluster_name}") public Response getCluster( @PathParam("cluster_name") String clusterName, - @QueryParam("limit") Optional limit) throws ReaperException { + @QueryParam("limit") Optional limit) + throws ReaperException { + LOG.debug("get cluster called with cluster_name: {}", clusterName); return viewCluster(clusterName, limit, Optional.absent()); } - private Response viewCluster(String clusterName, Optional limit, - Optional createdURI) throws ReaperException { + private Response viewCluster(String clusterName, Optional limit, Optional createdUri) + throws ReaperException { + Optional cluster = context.storage.getCluster(clusterName); if (!cluster.isPresent()) { return Response.status(Response.Status.NOT_FOUND) - .entity("cluster with name \"" + clusterName + "\" not found").build(); + .entity("cluster with name \"" + clusterName + "\" not found") + .build(); } else { - ClusterStatus view = - new ClusterStatus(cluster.get(), - context.storage.getClusterRunStatuses(clusterName, limit.or(Integer.MAX_VALUE)), - context.storage.getClusterScheduleStatuses(clusterName), getNodesStatus(cluster).orNull()); - if (createdURI.isPresent()) { - return Response.created(createdURI.get()) - .entity(view).build(); + ClusterStatus view = new ClusterStatus( + cluster.get(), + context.storage.getClusterRunStatuses(clusterName, limit.or(Integer.MAX_VALUE)), + context.storage.getClusterScheduleStatuses(clusterName), + getNodesStatus(cluster).orNull()); + if (createdUri.isPresent()) { + return Response.created(createdUri.get()).entity(view).build(); } else { - return Response.ok() - .entity(view).build(); + return Response.ok().entity(view).build(); } } } @@ -131,7 +136,9 @@ private Response viewCluster(String clusterName, Optional limit, @POST public Response addCluster( @Context UriInfo uriInfo, - @QueryParam("seedHost") Optional seedHost) throws ReaperException { + @QueryParam("seedHost") Optional seedHost) + throws ReaperException { + if (!seedHost.isPresent()) { LOG.error("POST on cluster resource called without seedHost"); return Response.status(400).entity("query parameter \"seedHost\" required").build(); @@ -144,12 +151,11 @@ public Response addCluster( } catch (java.lang.SecurityException e) { LOG.error(e.getMessage(), e); return Response.status(400) - .entity("seed host \"" + seedHost.get() + "\" JMX threw security exception: " - + e.getMessage()).build(); + .entity("seed host \"" + seedHost.get() + "\" JMX threw security exception: " + e.getMessage()) + .build(); } catch (ReaperException e) { LOG.error(e.getMessage(), e); - return Response.status(400) - .entity("failed to create cluster with seed host: " + seedHost.get()).build(); + return Response.status(400).entity("failed to create cluster with seed host: " + seedHost.get()).build(); } Optional existingCluster = context.storage.getCluster(newCluster.getName()); if (existingCluster.isPresent()) { @@ -167,22 +173,26 @@ public Response addCluster( } catch (ReaperException e) { LOG.error("failed to automatically schedule repairs", e); return Response.status(400) - .entity("failed to automatically schedule repairs for cluster with seed host \"" + seedHost.get() - + "\". Exception was: " + e.getMessage()).build(); + .entity( + "failed to automatically schedule repairs for cluster with seed host \"" + + seedHost.get() + + "\". Exception was: " + + e.getMessage()) + .build(); } } } - URI createdURI; + URI createdUri; try { - createdURI = new URL(uriInfo.getAbsolutePath().toURL(), newCluster.getName()).toURI(); - } catch (Exception e) { + createdUri = new URL(uriInfo.getAbsolutePath().toURL(), newCluster.getName()).toURI(); + } catch (MalformedURLException | URISyntaxException e) { String errMsg = "failed creating target URI for cluster: " + newCluster.getName(); LOG.error(errMsg, e); return Response.status(400).entity(errMsg).build(); } - return viewCluster(newCluster.getName(), Optional.absent(), Optional.of(createdURI)); + return viewCluster(newCluster.getName(), Optional.absent(), Optional.of(createdUri)); } public Cluster createClusterWithSeedHost(String seedHostInput) throws ReaperException { @@ -191,8 +201,8 @@ public Cluster createClusterWithSeedHost(String seedHostInput) throws ReaperExce Optional> liveNodes = Optional.absent(); Set seedHosts = CommonTools.parseSeedHosts(seedHostInput); - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { clusterName = Optional.of(jmxProxy.getClusterName()); partitioner = Optional.of(jmxProxy.getPartitioner()); @@ -201,15 +211,13 @@ public Cluster createClusterWithSeedHost(String seedHostInput) throws ReaperExce LOG.error("failed to create cluster with seed hosts: {}", seedHosts, e); } - if(!clusterName.isPresent()) { + if (!clusterName.isPresent()) { throw new ReaperException("Could not connect any seed host"); } Set seedHostsFinal = seedHosts; if (context.config.getEnableDynamicSeedList() && liveNodes.isPresent()) { - seedHostsFinal = !liveNodes.get().isEmpty() - ? liveNodes.get().stream().collect(Collectors.toSet()) - : seedHosts; + seedHostsFinal = !liveNodes.get().isEmpty() ? liveNodes.get().stream().collect(Collectors.toSet()) : seedHosts; } LOG.debug("Seeds {}", seedHostsFinal); @@ -222,7 +230,8 @@ public Cluster createClusterWithSeedHost(String seedHostInput) throws ReaperExce public Response modifyClusterSeed( @Context UriInfo uriInfo, @PathParam("cluster_name") String clusterName, - @QueryParam("seedHost") Optional seedHost) throws ReaperException { + @QueryParam("seedHost") Optional seedHost) + throws ReaperException { if (!seedHost.isPresent()) { LOG.error("PUT on cluster resource called without seedHost"); @@ -232,17 +241,16 @@ public Response modifyClusterSeed( Optional cluster = context.storage.getCluster(clusterName); if (!cluster.isPresent()) { - return Response - .status(Response.Status.NOT_FOUND) - .entity("cluster with name " + clusterName + " not found") - .build(); + return Response.status(Response.Status.NOT_FOUND) + .entity("cluster with name " + clusterName + " not found") + .build(); } Set newSeeds = CommonTools.parseSeedHosts(seedHost.get()); - if(context.config.getEnableDynamicSeedList()) { - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), newSeeds, context.config.getJmxConnectionTimeoutInSeconds())) { + if (context.config.getEnableDynamicSeedList()) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), newSeeds, context.config.getJmxConnectionTimeoutInSeconds())) { Optional> liveNodes = Optional.of(jmxProxy.getLiveNodes()); newSeeds = liveNodes.get().stream().collect(Collectors.toSet()); @@ -264,110 +272,105 @@ public Response modifyClusterSeed( /** * Delete a Cluster object with given name. * - * Cluster can be only deleted when it hasn't any RepairRun or RepairSchedule instances under it, - * i.e. you must delete all repair runs and schedules first. + *

+ * Cluster can be only deleted when it hasn't any RepairRun or RepairSchedule instances under it, i.e. you mus + * delete all repair runs and schedules first. * * @param clusterName The name of the Cluster instance you are about to delete. * @return The deleted RepairRun instance, with state overwritten to string "DELETED". - * @throws ReaperException */ @DELETE @Path("/{cluster_name}") - public Response deleteCluster(@PathParam("cluster_name") String clusterName) throws ReaperException { + public Response deleteCluster( + @PathParam("cluster_name") String clusterName) + throws ReaperException { LOG.info("delete cluster called with clusterName: {}", clusterName); Optional clusterToDelete = context.storage.getCluster(clusterName); if (!clusterToDelete.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).entity( - "cluster with name \"" + clusterName + "\" not found").build(); + return Response.status(Response.Status.NOT_FOUND) + .entity("cluster with name \"" + clusterName + "\" not found") + .build(); } if (!context.storage.getRepairSchedulesForCluster(clusterName).isEmpty()) { - return Response.status(Response.Status.FORBIDDEN).entity( - "cluster with name \"" + clusterName + "\" cannot be deleted, as it " - + "has repair schedules").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("cluster with name \"" + clusterName + "\" cannot be deleted, as it " + "has repair schedules") + .build(); } if (!context.storage.getRepairRunsForCluster(clusterName).isEmpty()) { - return Response.status(Response.Status.FORBIDDEN).entity( - "cluster with name \"" + clusterName + "\" cannot be deleted, as it " - + "has repair runs").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("cluster with name \"" + clusterName + "\" cannot be deleted, as it " + "has repair runs") + .build(); } Optional deletedCluster = context.storage.deleteCluster(clusterName); if (deletedCluster.isPresent()) { return Response.ok( - new ClusterStatus( - deletedCluster.get(), - Collections.emptyList(), - Collections.emptyList(), - getNodesStatus(deletedCluster).orNull())) + new ClusterStatus( + deletedCluster.get(), + Collections.emptyList(), + Collections.emptyList(), + getNodesStatus(deletedCluster).orNull())) .build(); } return Response.serverError().entity("delete failed for schedule with name \"" + clusterName + "\"").build(); } - /** * Callable to get and parse endpoint states through JMX * - * * @param seedHost The host address to connect to via JMX * @return An optional NodesStatus object with the status of each node in the cluster as seen from the seedHost node */ private Callable> getEndpointState(List seeds) { return () -> { - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), seeds, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), seeds, context.config.getJmxConnectionTimeoutInSeconds())) { Optional allEndpointsState = Optional.fromNullable(jmxProxy.getAllEndpointsState()); Optional> simpleStates = Optional.fromNullable(jmxProxy.getSimpleStates()); - return Optional - .of(new NodesStatus(jmxProxy.getHost(), allEndpointsState.or(""), simpleStates.or(new HashMap<>()))); + return Optional.of( + new NodesStatus(jmxProxy.getHost(), allEndpointsState.or(""), simpleStates.or(new HashMap<>()))); } catch (RuntimeException e) { LOG.debug("failed to create cluster with seed hosts: {}", seeds, e); - Thread.sleep(TimeUnit.MILLISECONDS.convert(JmxProxy.JMX_CONNECTION_TIMEOUT, JmxProxy.JMX_CONNECTION_TIMEOUT_UNIT)); + Thread.sleep( + TimeUnit.MILLISECONDS.convert(JmxProxy.JMX_CONNECTION_TIMEOUT, JmxProxy.JMX_CONNECTION_TIMEOUT_UNIT)); return Optional.absent(); } }; } - /** * Get all nodes state by querying the AllEndpointsState attribute through JMX. * + *

* To speed up execution, the method calls JMX on 3 nodes asynchronously and processes the first response * - * @param cluster * @return An optional NodesStatus object with all nodes statuses */ - public Optional getNodesStatus(Optional cluster){ + public Optional getNodesStatus(Optional cluster) { Optional nodesStatus = Optional.absent(); if (cluster.isPresent() && null != cluster.get().getSeedHosts()) { - List seedHosts = Lists.newArrayList(cluster.get().getSeedHosts()); + List seedHosts = Lists.newArrayList(cluster.get().getSeedHosts()); - List>> endpointStateTasks - = Lists.>>newArrayList( - getEndpointState(seedHosts), - getEndpointState(seedHosts), - getEndpointState(seedHosts)); + List>> endpointStateTasks = Lists.>>newArrayList( + getEndpointState(seedHosts), getEndpointState(seedHosts), getEndpointState(seedHosts)); - try { - nodesStatus = CLUSTER_STATUS_EXECUTOR.invokeAny( - endpointStateTasks, - JmxProxy.JMX_CONNECTION_TIMEOUT, - JmxProxy.JMX_CONNECTION_TIMEOUT_UNIT); + try { + nodesStatus = CLUSTER_STATUS_EXECUTOR.invokeAny( + endpointStateTasks, JmxProxy.JMX_CONNECTION_TIMEOUT, JmxProxy.JMX_CONNECTION_TIMEOUT_UNIT); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOG.debug("failed grabbing nodes status", e); - } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + LOG.debug("failed grabbing nodes status", e); + } - if (nodesStatus.isPresent()) { - return nodesStatus; - } + if (nodesStatus.isPresent()) { + return nodesStatus; } + } return nodesStatus; } - } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/CommonTools.java b/src/server/src/main/java/com/spotify/reaper/resources/CommonTools.java index d5e1344ec..fa6aff9c6 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/CommonTools.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/CommonTools.java @@ -1,6 +1,30 @@ +/* + * 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.spotify.reaper.resources; -import static com.google.common.base.Preconditions.checkNotNull; + +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.service.RingRange; +import com.spotify.reaper.service.SegmentGenerator; import java.math.BigInteger; import java.util.Arrays; @@ -13,15 +37,8 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; - import javax.annotation.Nullable; -import org.apache.cassandra.repair.RepairParallelism; -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import com.google.common.base.Optional; @@ -31,64 +48,67 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.service.SegmentGenerator; +import org.apache.cassandra.repair.RepairParallelism; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; public final class CommonTools { + public static final Splitter COMMA_SEPARATED_LIST_SPLITTER + = Splitter.on(',').trimResults(CharMatcher.anyOf(" ()[]\"'")).omitEmptyStrings(); + private static final Logger LOG = LoggerFactory.getLogger(CommonTools.class); + private CommonTools() { + } + /** * Creates a repair run but does not start it immediately. * - * Creating a repair run involves: - * 1) split token range into segments - * 2) create a RepairRun instance - * 3) create RepairSegment instances linked to RepairRun. + *

+ * Creating a repair run involves: 1) split token range into segments 2) create a RepairRun instance 3) create + * RepairSegment instances linked to RepairRun. * - * @throws com.spotify.reaper.ReaperException if repair run fails to be stored into Reaper's - * storage. + * @throws ReaperException if repair run fails to be stored into Reaper's storage. */ - public static RepairRun registerRepairRun(AppContext context, Cluster cluster, - RepairUnit repairUnit, Optional cause, - String owner, int segments, - RepairParallelism repairParallelism, Double intensity) + public static RepairRun registerRepairRun( + AppContext context, + Cluster cluster, + RepairUnit repairUnit, + Optional cause, + String owner, + int segments, + RepairParallelism repairParallelism, + Double intensity) throws ReaperException { // preparing a repair run involves several steps - // the first step is to generate token segments List tokenSegments = generateSegments(context, cluster, segments, repairUnit); checkNotNull(tokenSegments, "failed generating repair segments"); Map nodes = getClusterNodes(context, cluster, repairUnit); - // the next step is to prepare a repair run object + // the next step is to prepare a repair run objec segments = repairUnit.getIncrementalRepair() ? nodes.keySet().size() : tokenSegments.size(); RepairRun.Builder runBuilder - = createNewRepairRun(cluster, repairUnit, cause, owner, segments, repairParallelism, intensity); + = createNewRepairRun(cluster, repairUnit, cause, owner, segments, repairParallelism, intensity); // the last preparation step is to generate actual repair segments List segmentBuilders = repairUnit.getIncrementalRepair() - ? createRepairSegmentsForIncrementalRepair(nodes, repairUnit) - : createRepairSegments(tokenSegments, repairUnit); + ? createRepairSegmentsForIncrementalRepair(nodes, repairUnit) + : createRepairSegments(tokenSegments, repairUnit); RepairRun repairRun = context.storage.addRepairRun(runBuilder, segmentBuilders); - if (null == repairRun){ + if (null == repairRun) { String errMsg = String.format( - "failed storing repair run for cluster \"%s\", keyspace \"%s\", and column families: %s", - cluster.getName(), - repairUnit.getKeyspaceName(), - repairUnit.getColumnFamilies()); + "failed storing repair run for cluster \"%s\", keyspace \"%s\", and column families: %s", + cluster.getName(), repairUnit.getKeyspaceName(), repairUnit.getColumnFamilies()); LOG.error(errMsg); throw new ReaperException(errMsg); @@ -98,29 +118,33 @@ public static RepairRun registerRepairRun(AppContext context, Cluster cluster, /** * Splits a token range for given table into segments - * @param incrementalRepair * * @return the created segments - * @throws ReaperException when fails to discover seeds for the cluster or fails to connect to - * any of the nodes in the Cluster. + * @throws ReaperException when fails to discover seeds for the cluster or fails to connect to any of the nodes in the + * Cluster. */ - private static List generateSegments(AppContext context, Cluster targetCluster, - int segmentCount, RepairUnit repairUnit) - throws ReaperException { + private static List generateSegments( + AppContext context, + Cluster targetCluster, + int segmentCount, + RepairUnit repairUnit) throws ReaperException { + List segments = null; - Preconditions.checkState(targetCluster.getPartitioner() != null, + + Preconditions.checkNotNull( + targetCluster.getPartitioner(), "no partitioner for cluster: " + targetCluster.getName()); + SegmentGenerator sg = new SegmentGenerator(targetCluster.getPartitioner()); Set seedHosts = targetCluster.getSeedHosts(); if (seedHosts.isEmpty()) { - String errMsg = String.format("didn't get any seed hosts for cluster \"%s\"", - targetCluster.getName()); + String errMsg = String.format("didn't get any seed hosts for cluster \"%s\"", targetCluster.getName()); LOG.error(errMsg); throw new ReaperException(errMsg); } - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { List tokens = jmxProxy.getTokens(); Map, List> rangeToEndpoint = jmxProxy.getRangeToEndpointMap(repairUnit.getKeyspaceName()); @@ -128,14 +152,14 @@ private static List generateSegments(AppContext context, Cluster targ segments = filterSegmentsByNodes( sg.generateSegments(segmentCount, tokens, repairUnit.getIncrementalRepair()), - repairUnit, endpointToRange); + repairUnit, + endpointToRange); } catch (ReaperException e) { LOG.warn("couldn't connect to any host: {}, life sucks...", seedHosts, e); } if (segments == null || (segments.isEmpty() && !repairUnit.getIncrementalRepair())) { - String errMsg = String.format("failed to generate repair segments for cluster \"%s\"", - targetCluster.getName()); + String errMsg = String.format("failed to generate repair segments for cluster \"%s\"", targetCluster.getName()); LOG.error(errMsg); throw new ReaperException(errMsg); } @@ -143,26 +167,32 @@ private static List generateSegments(AppContext context, Cluster targ } @VisibleForTesting - public static List filterSegmentsByNodes(List segments, RepairUnit repairUnit, - Map> endpointToRange) throws ReaperException { + public static List filterSegmentsByNodes( + List segments, + RepairUnit repairUnit, + Map> endpointToRange) + throws ReaperException { + if (repairUnit.getNodes().isEmpty()) { return segments; } else { - return segments.stream().filter(segment -> { - for (Entry> entry : endpointToRange.entrySet()) { - if (repairUnit.getNodes().contains(entry.getKey())) { - for (RingRange range : entry.getValue()) { - if (range.encloses(segment)) { - return true; - } - } - } - } - return false; - }).collect(Collectors.toList()); - + return segments + .stream() + .filter( + segment -> { + for (Entry> entry : endpointToRange.entrySet()) { + if (repairUnit.getNodes().contains(entry.getKey())) { + for (RingRange range : entry.getValue()) { + if (range.encloses(segment)) { + return true; + } + } + } + } + return false; + }) + .collect(Collectors.toList()); } - } @VisibleForTesting @@ -194,43 +224,52 @@ private static RepairRun.Builder createNewRepairRun( String owner, int segments, RepairParallelism repairParallelism, - Double intensity) throws ReaperException { + Double intensity) + throws ReaperException { - return new RepairRun.Builder(cluster.getName(), repairUnit.getId(), DateTime.now(), intensity, segments, repairParallelism) + return new RepairRun.Builder( + cluster.getName(), repairUnit.getId(), DateTime.now(), intensity, segments, repairParallelism) .cause(cause.isPresent() ? cause.get() : "no cause specified") .owner(owner); } /** - * Creates the repair runs linked to given RepairRun and stores them directly in the storage - * backend. + * Creates the repair runs linked to given RepairRun and stores them directly in the storage backend. */ - private static List createRepairSegments(List tokenSegments, RepairUnit repairUnit){ + private static List createRepairSegments( + List tokenSegments, + RepairUnit repairUnit) { List repairSegmentBuilders = Lists.newArrayList(); tokenSegments.forEach(range -> repairSegmentBuilders.add(new RepairSegment.Builder(range, repairUnit.getId()))); return repairSegmentBuilders; } - /** - * Creates the repair runs linked to given RepairRun and stores them directly in the storage - * backend in case of incrementalRepair + * Creates the repair runs linked to given RepairRun and stores them directly in the storage backend in case of + * incrementalRepair */ private static List createRepairSegmentsForIncrementalRepair( - Map nodes, - RepairUnit repairUnit) { + Map nodes, + RepairUnit repairUnit) { List repairSegmentBuilders = Lists.newArrayList(); - nodes.entrySet().forEach(range - -> repairSegmentBuilders.add( - new RepairSegment.Builder(range.getValue(), repairUnit.getId()).coordinatorHost(range.getKey()))); + nodes + .entrySet() + .forEach( + range + -> repairSegmentBuilders.add( + new RepairSegment.Builder(range.getValue(), repairUnit.getId()).coordinatorHost(range.getKey()))); return repairSegmentBuilders; } - private static Map getClusterNodes(AppContext context, Cluster targetCluster, RepairUnit repairUnit) throws ReaperException { + private static Map getClusterNodes( + AppContext context, + Cluster targetCluster, + RepairUnit repairUnit) throws ReaperException { + ConcurrentHashMap nodesWithRanges = new ConcurrentHashMap<>(); Set seedHosts = targetCluster.getSeedHosts(); if (seedHosts.isEmpty()) { @@ -241,16 +280,16 @@ private static Map getClusterNodes(AppContext context, Clust Map, List> rangeToEndpoint = Maps.newHashMap(); - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), seedHosts, context.config.getJmxConnectionTimeoutInSeconds())) { rangeToEndpoint = jmxProxy.getRangeToEndpointMap(repairUnit.getKeyspaceName()); } catch (ReaperException e) { - LOG.error("couldn't connect to any host: {}, will try next one", e); - throw new ReaperException(e); + LOG.error("couldn't connect to any host: {}, will try next one", e); + throw new ReaperException(e); } - for (Entry, List> tokenRangeToEndpoint:rangeToEndpoint.entrySet()) { + for (Entry, List> tokenRangeToEndpoint : rangeToEndpoint.entrySet()) { String node = tokenRangeToEndpoint.getValue().get(0); RingRange range = new RingRange(tokenRangeToEndpoint.getKey().get(0), tokenRangeToEndpoint.getKey().get(1)); nodesWithRanges.putIfAbsent(node, range); @@ -259,7 +298,6 @@ private static Map getClusterNodes(AppContext context, Clust return nodesWithRanges; } - /** * Instantiates a RepairSchedule and stores it in the storage backend. * @@ -277,23 +315,36 @@ public static RepairSchedule storeNewRepairSchedule( RepairParallelism repairParallelism, Double intensity) throws ReaperException { - RepairSchedule.Builder scheduleBuilder = - new RepairSchedule.Builder(repairUnit.getId(), RepairSchedule.State.ACTIVE, daysBetween, - nextActivation, ImmutableList.of(), segments, - repairParallelism, intensity, - DateTime.now()); + + RepairSchedule.Builder scheduleBuilder = new RepairSchedule.Builder( + repairUnit.getId(), + RepairSchedule.State.ACTIVE, + daysBetween, + nextActivation, + ImmutableList.of(), + segments, + repairParallelism, + intensity, + DateTime.now()); + scheduleBuilder.owner(owner); - Collection repairSchedules = context.storage.getRepairSchedulesForClusterAndKeyspace(repairUnit.getClusterName(), repairUnit.getKeyspaceName()); - for(RepairSchedule sched:repairSchedules){ + Collection repairSchedules = context.storage + .getRepairSchedulesForClusterAndKeyspace(repairUnit.getClusterName(), repairUnit.getKeyspaceName()); + + for (RepairSchedule sched : repairSchedules) { Optional repairUnitForSched = context.storage.getRepairUnit(sched.getRepairUnitId()); - if(repairUnitForSched.isPresent() && repairUnitForSched.get().getClusterName().equals(repairUnit.getClusterName()) && repairUnitForSched.get().getKeyspaceName().equals(repairUnit.getKeyspaceName()) && repairUnitForSched.get().getIncrementalRepair().equals(repairUnit.getIncrementalRepair())){ - if(CommonTools.aConflictingScheduleAlreadyExists(repairUnitForSched.get(), repairUnit)){ - String errMsg = String.format("A repair schedule already exists for cluster \"%s\", " - + "keyspace \"%s\", and column families: %s", - cluster.getName(), repairUnit.getKeyspaceName(), - Sets.intersection(repairUnit.getColumnFamilies(),repairUnitForSched.get().getColumnFamilies())); - LOG.error(errMsg); + if (repairUnitForSched.isPresent() + && repairUnitForSched.get().getClusterName().equals(repairUnit.getClusterName()) + && repairUnitForSched.get().getKeyspaceName().equals(repairUnit.getKeyspaceName()) + && repairUnitForSched.get().getIncrementalRepair().equals(repairUnit.getIncrementalRepair())) { + if (CommonTools.isConflictingSchedules(repairUnitForSched.get(), repairUnit)) { + String errMsg = String.format( + "A repair schedule already exists for cluster \"%s\", " + "keyspace \"%s\", and column families: %s", + cluster.getName(), + repairUnit.getKeyspaceName(), + Sets.intersection(repairUnit.getColumnFamilies(), repairUnitForSched.get().getColumnFamilies())); + LOG.error(errMsg); throw new ReaperException(errMsg); } } @@ -301,32 +352,31 @@ public static RepairSchedule storeNewRepairSchedule( RepairSchedule newRepairSchedule = context.storage.addRepairSchedule(scheduleBuilder); if (newRepairSchedule == null) { - String errMsg = String.format("failed storing repair schedule for cluster \"%s\", " - + "keyspace \"%s\", and column families: %s", - cluster.getName(), repairUnit.getKeyspaceName(), - repairUnit.getColumnFamilies()); + String errMsg = String.format( + "failed storing repair schedule for cluster \"%s\", " + "keyspace \"%s\", and column families: %s", + cluster.getName(), repairUnit.getKeyspaceName(), repairUnit.getColumnFamilies()); LOG.error(errMsg); throw new ReaperException(errMsg); } return newRepairSchedule; } - private static final boolean aConflictingScheduleAlreadyExists(RepairUnit newRepairUnit, RepairUnit existingRepairUnit){ + private static boolean isConflictingSchedules(RepairUnit newRepairUnit, RepairUnit existingRepairUnit) { return (newRepairUnit.getColumnFamilies().isEmpty() && existingRepairUnit.getColumnFamilies().isEmpty()) || newRepairUnit.getColumnFamilies().isEmpty() && !existingRepairUnit.getColumnFamilies().isEmpty() || !newRepairUnit.getColumnFamilies().isEmpty() && existingRepairUnit.getColumnFamilies().isEmpty() - || !Sets.intersection(existingRepairUnit.getColumnFamilies(),newRepairUnit.getColumnFamilies()).isEmpty(); - + || !Sets.intersection(existingRepairUnit.getColumnFamilies(), newRepairUnit.getColumnFamilies()).isEmpty(); } - public static final Splitter COMMA_SEPARATED_LIST_SPLITTER = - Splitter.on(',').trimResults(CharMatcher.anyOf(" ()[]\"'")).omitEmptyStrings(); - public static Set getTableNamesBasedOnParam( - AppContext context, Cluster cluster, String keyspace, Optional tableNamesParam) - throws ReaperException { + AppContext context, + Cluster cluster, + String keyspace, + Optional tableNamesParam) throws ReaperException { + Set knownTables; - try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy + = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { knownTables = jmxProxy.getTableNamesForKeyspace(keyspace); if (knownTables.isEmpty()) { LOG.debug("no known tables for keyspace {} in cluster {}", keyspace, cluster.getName()); @@ -338,8 +388,7 @@ public static Set getTableNamesBasedOnParam( tableNames = Sets.newHashSet(COMMA_SEPARATED_LIST_SPLITTER.split(tableNamesParam.get())); for (String name : tableNames) { if (!knownTables.contains(name)) { - throw new IllegalArgumentException("keyspace doesn't contain a table named \"" - + name + "\""); + throw new IllegalArgumentException("keyspace doesn't contain a table named \"" + name + "\""); } } } else { @@ -348,11 +397,14 @@ public static Set getTableNamesBasedOnParam( return tableNames; } - public static Set getNodesToRepairBasedOnParam(AppContext context, Cluster cluster, + public static Set getNodesToRepairBasedOnParam( + AppContext context, + Cluster cluster, Optional nodesToRepairParam) throws ReaperException { + Set nodesInCluster; - try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny(cluster, - context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy + = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { nodesInCluster = jmxProxy.getEndpointToHostId().keySet(); if (nodesInCluster.isEmpty()) { LOG.debug("no nodes found in cluster {}", cluster.getName()); @@ -374,7 +426,9 @@ public static Set getNodesToRepairBasedOnParam(AppContext context, Clust return nodesToRepair; } - public static Set getDatacentersToRepairBasedOnParam(AppContext context, Cluster cluster, + public static Set getDatacentersToRepairBasedOnParam( + AppContext context, + Cluster cluster, Optional datacenters) throws ReaperException { Set datacentersToRepair; @@ -386,49 +440,67 @@ public static Set getDatacentersToRepairBasedOnParam(AppContext context, return datacentersToRepair; } - public static RepairUnit getNewOrExistingRepairUnit(AppContext context, Cluster cluster, - String keyspace, Set tableNames, Boolean incrementalRepair, - Set nodesToRepair, Set datacenters) throws ReaperException { - Optional storedRepairUnit = - context.storage.getRepairUnit(cluster.getName(), keyspace, tableNames); + public static RepairUnit getNewOrExistingRepairUnit( + AppContext context, + Cluster cluster, + String keyspace, + Set tableNames, + Boolean incrementalRepair, + Set nodesToRepair, + Set datacenters) + throws ReaperException { + + Optional storedRepairUnit = context.storage.getRepairUnit(cluster.getName(), keyspace, tableNames); RepairUnit theRepairUnit; Optional cassandraVersion = Optional.absent(); - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connectAny(Optional.absent(), cluster.getSeedHosts(), context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny( + Optional.absent(), cluster.getSeedHosts(), context.config.getJmxConnectionTimeoutInSeconds())) { cassandraVersion = Optional.fromNullable(jmxProxy.getCassandraVersion()); } catch (ReaperException e) { LOG.warn("couldn't connect to hosts: {}, life sucks...", cluster.getSeedHosts(), e); } - if(cassandraVersion.isPresent() && cassandraVersion.get().startsWith("2.0") && incrementalRepair){ + if (cassandraVersion.isPresent() && cassandraVersion.get().startsWith("2.0") && incrementalRepair) { String errMsg = "Incremental repair does not work with Cassandra versions before 2.1"; LOG.error(errMsg); throw new ReaperException(errMsg); } - if (storedRepairUnit.isPresent() && storedRepairUnit.get().getIncrementalRepair().equals(incrementalRepair) + if (storedRepairUnit.isPresent() + && storedRepairUnit.get().getIncrementalRepair().equals(incrementalRepair) && storedRepairUnit.get().getNodes().equals(nodesToRepair) && storedRepairUnit.get().getDatacenters().equals(datacenters)) { LOG.info( - "use existing repair unit for cluster '{}', keyspace '{}', column families: {}, nodes: {} and datacenters: {}", - cluster.getName(), keyspace, tableNames, nodesToRepair, datacenters); + "use existing repair unit for cluster '{}', keyspace '{}', " + + "column families: {}, nodes: {} and datacenters: {}", + cluster.getName(), + keyspace, + tableNames, + nodesToRepair, + datacenters); theRepairUnit = storedRepairUnit.get(); } else { LOG.info( "create new repair unit for cluster '{}', keyspace '{}', column families: {}, nodes: {} and datacenters: {}", - cluster.getName(), keyspace, tableNames, nodesToRepair, datacenters); + cluster.getName(), + keyspace, + tableNames, + nodesToRepair, + datacenters); theRepairUnit = context.storage.addRepairUnit( - new RepairUnit.Builder(cluster.getName(), keyspace, tableNames, incrementalRepair, nodesToRepair, - datacenters)); + new RepairUnit.Builder( + cluster.getName(), keyspace, tableNames, incrementalRepair, nodesToRepair, datacenters)); } return theRepairUnit; } @Nullable - public static String dateTimeToISO8601(@Nullable DateTime dateTime) { + public static String dateTimeToIso8601( + @Nullable DateTime dateTime) { + if (null == dateTime) { return null; } @@ -443,6 +515,4 @@ public static Set parseSeedHosts(String seedHost) { return Arrays.stream(seedHost.split(",")).map(String::trim).collect(Collectors.toSet()); } - private CommonTools(){} - } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/PingResource.java b/src/server/src/main/java/com/spotify/reaper/resources/PingResource.java index 37cf452fd..09ac65fbb 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/PingResource.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/PingResource.java @@ -11,26 +11,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.ws.rs.GET; +import javax.ws.rs.HEAD; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Path("/ping") @Produces(MediaType.TEXT_PLAIN) -public class PingResource { +public final class PingResource { private static final Logger LOG = LoggerFactory.getLogger(ClusterResource.class); - @GET - public String answerPing() { + @HEAD + public Response headPing() { LOG.debug("ping called"); - return "Cassandra Reaper ping resource: PONG"; + return Response.noContent().build(); } + @GET + public Response getPing() { + LOG.debug("ping called"); + return Response.noContent().build(); + } } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/ReaperHealthCheck.java b/src/server/src/main/java/com/spotify/reaper/resources/ReaperHealthCheck.java index 5565bf2d0..d27d677aa 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/ReaperHealthCheck.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/ReaperHealthCheck.java @@ -1,3 +1,17 @@ +/* + * 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.spotify.reaper.resources; import com.spotify.reaper.AppContext; @@ -5,9 +19,9 @@ /** * Provides an endpoint to check the health of the running Reaper instance. */ -public class ReaperHealthCheck extends com.codahale.metrics.health.HealthCheck { +public final class ReaperHealthCheck extends com.codahale.metrics.health.HealthCheck { - private AppContext context; + private final AppContext context; public ReaperHealthCheck(AppContext context) { this.context = context; diff --git a/src/server/src/main/java/com/spotify/reaper/resources/RepairRunResource.java b/src/server/src/main/java/com/spotify/reaper/resources/RepairRunResource.java index a1ece5544..d57dc9985 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/RepairRunResource.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/RepairRunResource.java @@ -11,9 +11,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources; -import static com.google.common.base.Preconditions.checkNotNull; + +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairRun.RunState; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.view.RepairRunStatus; import java.net.MalformedURLException; import java.net.URI; @@ -25,7 +34,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; - import javax.annotation.Nullable; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -40,27 +48,20 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import org.apache.cassandra.repair.RepairParallelism; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairRun.RunState; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.view.RepairRunStatus; +import org.apache.cassandra.repair.RepairParallelism; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; @Path("/repair_run") @Produces(MediaType.APPLICATION_JSON) -public class RepairRunResource { +public final class RepairRunResource { private static final Logger LOG = LoggerFactory.getLogger(RepairRunResource.class); @@ -71,16 +72,15 @@ public RepairRunResource(AppContext context) { } /** - * Endpoint used to create a repair run. Does not allow triggering the run. - * triggerRepairRun() must be called to initiate the repair. - * Creating a repair run includes generating the repair segments. + * Endpoint used to create a repair run. Does not allow triggering the run. triggerRepairRun() must be called to + * initiate the repair. Creating a repair run includes generating the repair segments. * - * Notice that query parameter "tables" can be a single String, or a - * comma-separated list of table names. If the "tables" parameter is omitted, and only the - * keyspace is defined, then created repair run will target all the tables in the keyspace. + *

+ * Notice that query parameter "tables" can be a single String, or a comma-separated list of table names. If the + * "tables" parameter is omitted, and only the keyspace is defined, then created repair run will target all the tables + * in the keyspace. * - * @return repair run ID in case of everything going well, - * and a status code 500 in case of errors. + * @return repair run ID in case of everything going well, and a status code 500 in case of errors. */ @POST public Response addRepairRun( @@ -95,17 +95,35 @@ public Response addRepairRun( @QueryParam("intensity") Optional intensityStr, @QueryParam("incrementalRepair") Optional incrementalRepairStr, @QueryParam("nodes") Optional nodesToRepairParam, - @QueryParam("datacenters") Optional datacentersToRepairParam - ) { - LOG.info("add repair run called with: clusterName = {}, keyspace = {}, tables = {}, owner = {}," + @QueryParam("datacenters") Optional datacentersToRepairParam) { + + LOG.info( + "add repair run called with: clusterName = {}, keyspace = {}, tables = {}, owner = {}," + " cause = {}, segmentCount = {}, repairParallelism = {}, intensity = {}, incrementalRepair = {}," + " nodes = {}, datacenters = {} ", - clusterName, keyspace, tableNamesParam, owner, cause, segmentCount, repairParallelism, - intensityStr, incrementalRepairStr, nodesToRepairParam, datacentersToRepairParam); + clusterName, + keyspace, + tableNamesParam, + owner, + cause, + segmentCount, + repairParallelism, + intensityStr, + incrementalRepairStr, + nodesToRepairParam, + datacentersToRepairParam); try { final Response possibleFailedResponse = RepairRunResource.checkRequestForAddRepair( - context, clusterName, keyspace, owner, segmentCount, repairParallelism, intensityStr, incrementalRepairStr, - nodesToRepairParam, datacentersToRepairParam); + context, + clusterName, + keyspace, + owner, + segmentCount, + repairParallelism, + intensityStr, + incrementalRepairStr, + nodesToRepairParam, + datacentersToRepairParam); if (null != possibleFailedResponse) { return possibleFailedResponse; } @@ -126,12 +144,13 @@ public Response addRepairRun( LOG.debug("no incremental repair given, so using default value: {}", incrementalRepair); } - int segments = context.config.getSegmentCount(); if (!incrementalRepair) { if (segmentCount.isPresent()) { - LOG.debug("using given segment count {} instead of configured value {}", - segmentCount.get(), context.config.getSegmentCount()); + LOG.debug( + "using given segment count {} instead of configured value {}", + segmentCount.get(), + context.config.getSegmentCount()); segments = segmentCount.get(); } } else { @@ -140,15 +159,11 @@ public Response addRepairRun( segments = -1; } - - - final Cluster cluster = context.storage.getCluster(Cluster.toSymbolicName(clusterName.get())).get(); Set tableNames; try { - tableNames = CommonTools.getTableNamesBasedOnParam(context, cluster, - keyspace.get(), tableNamesParam); - } catch (final IllegalArgumentException ex) { + tableNames = CommonTools.getTableNamesBasedOnParam(context, cluster, keyspace.get(), tableNamesParam); + } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } @@ -156,34 +171,48 @@ public Response addRepairRun( final Set nodesToRepair; try { nodesToRepair = CommonTools.getNodesToRepairBasedOnParam(context, cluster, nodesToRepairParam); - } catch (final IllegalArgumentException ex) { + } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } final Set datacentersToRepair; try { - datacentersToRepair = CommonTools.getDatacentersToRepairBasedOnParam(context, cluster, - datacentersToRepairParam); - } catch (final IllegalArgumentException ex) { + datacentersToRepair = CommonTools + .getDatacentersToRepairBasedOnParam(context, cluster, datacentersToRepairParam); + + } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } - final RepairUnit theRepairUnit = - CommonTools.getNewOrExistingRepairUnit(context, cluster, keyspace.get(), tableNames, incrementalRepair, - nodesToRepair, datacentersToRepair); - - if (theRepairUnit.getIncrementalRepair() != incrementalRepair) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "A repair run already exist for the same cluster/keyspace/table but with a different incremental repair value." - + "Requested value: " + incrementalRepair + " | Existing value: " + theRepairUnit.getIncrementalRepair()).build(); + final RepairUnit theRepairUnit = CommonTools.getNewOrExistingRepairUnit( + context, + cluster, + keyspace.get(), + tableNames, + incrementalRepair, + nodesToRepair, + datacentersToRepair); + + if (theRepairUnit.getIncrementalRepair().booleanValue() != incrementalRepair) { + return Response.status(Response.Status.BAD_REQUEST) + .entity( + "A repair run already exist for the same cluster/keyspace/table" + + " but with a different incremental repair value. Requested value: " + + incrementalRepair + + " | Existing value: " + + theRepairUnit.getIncrementalRepair()) + .build(); } RepairParallelism parallelism = context.config.getRepairParallelism(); if (repairParallelism.isPresent()) { - LOG.debug("using given repair parallelism {} instead of configured value {}", - repairParallelism.get(), context.config.getRepairParallelism()); + LOG.debug( + "using given repair parallelism {} instead of configured value {}", + repairParallelism.get(), + context.config.getRepairParallelism()); + parallelism = RepairParallelism.valueOf(repairParallelism.get().toUpperCase()); } @@ -191,14 +220,14 @@ public Response addRepairRun( parallelism = RepairParallelism.PARALLEL; } - final RepairRun newRepairRun = CommonTools.registerRepairRun( - context, cluster, theRepairUnit, cause, owner.get(), segments, - parallelism, intensity); + final RepairRun newRepairRun = CommonTools + .registerRepairRun(context, cluster, theRepairUnit, cause, owner.get(), segments, parallelism, intensity); - return Response.created(buildRepairRunURI(uriInfo, newRepairRun)) - .entity(new RepairRunStatus(newRepairRun, theRepairUnit, 0)).build(); + return Response.created(buildRepairRunUri(uriInfo, newRepairRun)) + .entity(new RepairRunStatus(newRepairRun, theRepairUnit, 0)) + .build(); - } catch (final ReaperException e) { + } catch (ReaperException e) { LOG.error(e.getMessage(), e); return Response.status(500).entity(e.getMessage()).build(); } @@ -209,30 +238,35 @@ public Response addRepairRun( */ @Nullable public static Response checkRequestForAddRepair( - AppContext context, Optional clusterName, Optional keyspace, - Optional owner, Optional segmentCount, - Optional repairParallelism, Optional intensityStr, Optional incrementalRepairStr, - Optional nodesStr, Optional datacentersStr) { + AppContext context, + Optional clusterName, + Optional keyspace, + Optional owner, + Optional segmentCount, + Optional repairParallelism, + Optional intensityStr, + Optional incrementalRepairStr, + Optional nodesStr, + Optional datacentersStr) { + if (!clusterName.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "missing query parameter \"clusterName\"").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("missing query parameter \"clusterName\"").build(); } if (!keyspace.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "missing query parameter \"keyspace\"").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("missing query parameter \"keyspace\"").build(); } if (!owner.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "missing query parameter \"owner\"").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("missing query parameter \"owner\"").build(); } if (segmentCount.isPresent() && (segmentCount.get() < 1 || segmentCount.get() > 100000)) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "invalid query parameter \"segmentCount\", maximum value is 100000").build(); + return Response.status(Response.Status.BAD_REQUEST) + .entity("invalid query parameter \"segmentCount\", maximum value is 100000") + .build(); } if (repairParallelism.isPresent()) { try { checkRepairParallelismString(repairParallelism.get()); - } catch (final ReaperException ex) { + } catch (ReaperException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.BAD_REQUEST).entity(ex.getMessage()).build(); } @@ -241,26 +275,29 @@ public static Response checkRequestForAddRepair( try { final Double intensity = Double.parseDouble(intensityStr.get()); if (intensity <= 0.0 || intensity > 1.0) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "query parameter \"intensity\" must be in half closed range (0.0, 1.0]: " - + intensityStr.get()).build(); + return Response.status(Response.Status.BAD_REQUEST) + .entity("query parameter \"intensity\" must be in half closed range (0.0, 1.0]: " + intensityStr.get()) + .build(); } - } catch (final NumberFormatException ex) { + } catch (NumberFormatException ex) { LOG.error(ex.getMessage(), ex); - return Response.status(Response.Status.BAD_REQUEST).entity( - "invalid value for query parameter \"intensity\": " + intensityStr.get()).build(); + return Response.status(Response.Status.BAD_REQUEST) + .entity("invalid value for query parameter \"intensity\": " + intensityStr.get()) + .build(); } } - if (incrementalRepairStr.isPresent() && (!incrementalRepairStr.get().toUpperCase().contentEquals("TRUE" ) && !incrementalRepairStr.get().toUpperCase().contentEquals("FALSE")) ) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "invalid query parameter \"incrementalRepair\", expecting [True,False]").build(); + if (incrementalRepairStr.isPresent() + && (!incrementalRepairStr.get().toUpperCase().contentEquals("TRUE") + && !incrementalRepairStr.get().toUpperCase().contentEquals("FALSE"))) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("invalid query parameter \"incrementalRepair\", expecting [True,False]") + .build(); } - final Optional cluster = - context.storage.getCluster(Cluster.toSymbolicName(clusterName.get())); + final Optional cluster = context.storage.getCluster(Cluster.toSymbolicName(clusterName.get())); if (!cluster.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).entity( - "No cluster found with name \"" + clusterName.get() - + "\", did you register your cluster first?").build(); + return Response.status(Response.Status.NOT_FOUND) + .entity("No cluster found with name \"" + clusterName.get() + "\", did you register your cluster first?") + .build(); } if (!datacentersStr.or("").isEmpty() && !nodesStr.or("").isEmpty()) { @@ -274,69 +311,68 @@ public static Response checkRequestForAddRepair( } /** - * Modifies a state of the repair run.

Currently supports NOT_STARTED|PAUSED -> RUNNING and - * RUNNING -> PAUSED. + * Modifies a state of the repair run. + * + *

+ * Currently supports NOT_STARTED|PAUSED -> RUNNING and RUNNING -> PAUSED. * - * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 501 - * (NOT_IMPLEMENTED) if transition is not supported. - * @throws ReaperException + * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 501 (NOT_IMPLEMENTED) if + * transition is not supported. */ @PUT @Path("/{id}") public Response modifyRunState( @Context UriInfo uriInfo, @PathParam("id") UUID repairRunId, - @QueryParam("state") Optional state) throws ReaperException { + @QueryParam("state") Optional state) + throws ReaperException { LOG.info("modify repair run state called with: id = {}, state = {}", repairRunId, state); if (!state.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) - .entity("\"state\" argument missing").build(); + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).entity("\"state\" argument missing").build(); } final Optional repairRun = context.storage.getRepairRun(repairRunId); if (!repairRun.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).entity("repair run with id " - + repairRunId + " not found") + return Response.status(Response.Status.NOT_FOUND) + .entity("repair run with id " + repairRunId + " not found") .build(); } - final Optional repairUnit = - context.storage.getRepairUnit(repairRun.get().getRepairUnitId()); + final Optional repairUnit = context.storage.getRepairUnit(repairRun.get().getRepairUnitId()); if (!repairUnit.isPresent()) { final String errMsg = "repair unit with id " + repairRun.get().getRepairUnitId() + " not found"; LOG.error(errMsg); return Response.status(Response.Status.NOT_FOUND).entity(errMsg).build(); } - // Check that no other repair run exists with a status different than DONE for this repair unit + // Check that no other repair run exists with a status different than DONE for this repair uni final Collection repairRuns = context.storage.getRepairRunsForUnit(repairRun.get().getRepairUnitId()); - for(final RepairRun run:repairRuns){ - if(!run.getId().equals(repairRunId) && run.getRunState().equals(RunState.RUNNING)){ + for (final RepairRun run : repairRuns) { + if (!run.getId().equals(repairRunId) && run.getRunState().equals(RunState.RUNNING)) { final String errMsg = "repair unit already has run " + run.getId() + " in RUNNING state"; LOG.error(errMsg); return Response.status(Response.Status.CONFLICT).entity(errMsg).build(); } } - - final int segmentsRepaired = - context.storage.getSegmentAmountForRepairRunWithState(repairRunId, RepairSegment.State.DONE); + final int segmentsRepaired + = context.storage.getSegmentAmountForRepairRunWithState(repairRunId, RepairSegment.State.DONE); RepairRun.RunState newState; try { newState = RepairRun.RunState.valueOf(state.get().toUpperCase()); - } catch (final IllegalArgumentException ex) { + } catch (IllegalArgumentException ex) { return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) - .entity("invalid \"state\" argument: " + state.get()).build(); + .entity("invalid \"state\" argument: " + state.get()) + .build(); } final RepairRun.RunState oldState = repairRun.get().getRunState(); if (oldState == newState) { - return Response - .status(Response.Status.NOT_MODIFIED) + return Response.status(Response.Status.NOT_MODIFIED) .entity("given \"state\" is same as the current run state") .build(); } @@ -350,8 +386,7 @@ public Response modifyRunState( } else if (isAborting(oldState, newState)) { return abortRun(repairRun.get(), repairUnit.get(), segmentsRepaired); } else { - final String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(), - newState.toString()); + final String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(), newState.toString()); LOG.error(errMsg); return Response.status(Response.Status.METHOD_NOT_ALLOWED).entity(errMsg).build(); } @@ -380,8 +415,8 @@ private boolean isAborting(RepairRun.RunState oldState, RepairRun.RunState newSt private Response startRun(RepairRun repairRun, RepairUnit repairUnit, int segmentsRepaired) throws ReaperException { LOG.info("Starting run {}", repairRun.getId()); final RepairRun newRun = context.repairManager.startRepairRun(context, repairRun); - return Response.status(Response.Status.OK).entity( - new RepairRunStatus(newRun, repairUnit, segmentsRepaired)) + return Response.status(Response.Status.OK) + .entity(new RepairRunStatus(newRun, repairUnit, segmentsRepaired)) .build(); } @@ -408,14 +443,15 @@ private Response abortRun(RepairRun repairRun, RepairUnit repairUnit, int segmen */ @GET @Path("/{id}") - public Response getRepairRun(@PathParam("id") UUID repairRunId) { + public Response getRepairRun( + @PathParam("id") UUID repairRunId) { + LOG.debug("get repair_run called with: id = {}", repairRunId); final Optional repairRun = context.storage.getRepairRun(repairRunId); if (repairRun.isPresent()) { return Response.ok().entity(getRepairRunStatus(repairRun.get())).build(); } else { - return Response.status(404).entity( - "repair run with id " + repairRunId + " doesn't exist").build(); + return Response.status(404).entity("repair run with id " + repairRunId + " doesn't exist").build(); } } @@ -424,7 +460,9 @@ public Response getRepairRun(@PathParam("id") UUID repairRunId) { */ @GET @Path("/cluster/{cluster_name}") - public Response getRepairRunsForCluster(@PathParam("cluster_name") String clusterName) { + public Response getRepairRunsForCluster( + @PathParam("cluster_name") String clusterName) { + LOG.debug("get repair run for cluster called with: cluster_name = {}", clusterName); final Collection repairRuns = context.storage.getRepairRunsForCluster(clusterName); final Collection repairRunViews = new ArrayList<>(); @@ -440,9 +478,8 @@ public Response getRepairRunsForCluster(@PathParam("cluster_name") String cluste private RepairRunStatus getRepairRunStatus(RepairRun repairRun) { final Optional repairUnit = context.storage.getRepairUnit(repairRun.getRepairUnitId()); Preconditions.checkState(repairUnit.isPresent(), "no repair unit found with id: %s", repairRun.getRepairUnitId()); - final int segmentsRepaired = - context.storage.getSegmentAmountForRepairRunWithState(repairRun.getId(), - RepairSegment.State.DONE); + final int segmentsRepaired + = context.storage.getSegmentAmountForRepairRunWithState(repairRun.getId(), RepairSegment.State.DONE); return new RepairRunStatus(repairRun, repairUnit.get(), segmentsRepaired); } @@ -451,7 +488,7 @@ private RepairRunStatus getRepairRunStatus(RepairRun repairRun) { * * @return The created resource URI. */ - private URI buildRepairRunURI(UriInfo uriInfo, RepairRun repairRun) { + private URI buildRepairRunUri(UriInfo uriInfo, RepairRun repairRun) { final String newRepairRunPathPart = "repair_run/" + repairRun.getId(); URI runUri = null; try { @@ -464,14 +501,16 @@ private URI buildRepairRunURI(UriInfo uriInfo, RepairRun repairRun) { } /** - * @param state comma-separated list of states to return. These states must match names of - * {@link com.spotify.reaper.core.RepairRun.RunState}. - * @return All repair runs in the system if the param is absent, repair runs with state included - * in the state parameter otherwise. If the state parameter contains non-existing run states, - * BAD_REQUEST response is returned. + * @param state comma-separated list of states to return. These states must match names of {@link + * com.spotify.reaper.core.RepairRun.RunState}. + * @return All repair runs in the system if the param is absent, repair runs with state included in the state + * parameter otherwise. + * If the state parameter contains non-existing run states, BAD_REQUEST response is returned. */ @GET - public Response listRepairRuns(@QueryParam("state") Optional state) { + public Response listRepairRuns( + @QueryParam("state") Optional state) { + try { final List runStatuses = Lists.newArrayList(); final Set desiredStates = splitStateParam(state); @@ -487,7 +526,7 @@ public Response listRepairRuns(@QueryParam("state") Optional state) { } return Response.status(Response.Status.OK).entity(runStatuses).build(); - } catch (final Exception e) { + } catch (ReaperException e) { LOG.error("Failed listing cluster statuses", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } @@ -503,14 +542,13 @@ private List getRunStatuses(Collection runs, Set des if (runsUnit.isPresent()) { int segmentsRepaired = run.getSegmentCount(); if (!run.getRunState().equals(RepairRun.RunState.DONE)) { - segmentsRepaired = context.storage.getSegmentAmountForRepairRunWithState(run.getId(), - RepairSegment.State.DONE); + segmentsRepaired + = context.storage.getSegmentAmountForRepairRunWithState(run.getId(), RepairSegment.State.DONE); } runStatuses.add(new RepairRunStatus(run, runsUnit.get(), segmentsRepaired)); } else { - final String errMsg = - String.format("Found repair run %d with no associated repair unit", run.getId()); + final String errMsg = String.format("Found repair run %s with no associated repair unit", run.getId()); LOG.error(errMsg); throw new ReaperException("Internal server error : " + errMsg); } @@ -526,7 +564,7 @@ public Set splitStateParam(Optional state) { for (final String chunk : chunks) { try { RepairRun.RunState.valueOf(chunk.toUpperCase()); - } catch (final IllegalArgumentException e) { + } catch (IllegalArgumentException e) { LOG.warn("Listing repair runs called with erroneous states: {}", state.get(), e); return null; } @@ -540,64 +578,78 @@ public Set splitStateParam(Optional state) { /** * Delete a RepairRun object with given id. * - * Repair run can be only deleted when it is not running. - * When Repair run is deleted, all the related RepairSegment instances will be deleted also. + *

+ * Repair run can be only deleted when it is not running. When Repair run is deleted, all the related RepairSegmen + * instances will be deleted also. * - * @param runId The id for the RepairRun instance to delete. - * @param owner The assigned owner of the deleted resource. Must match the stored one. + * @param runId The id for the RepairRun instance to delete. + * @param owner The assigned owner of the deleted resource. Must match the stored one. * @return The deleted RepairRun instance, with state overwritten to string "DELETED". */ @DELETE @Path("/{id}") - public Response deleteRepairRun(@PathParam("id") UUID runId, + public Response deleteRepairRun( + @PathParam("id") UUID runId, @QueryParam("owner") Optional owner) { + LOG.info("delete repair run called with runId: {}, and owner: {}", runId, owner); if (!owner.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "required query parameter \"owner\" is missing").build(); + return Response.status(Response.Status.BAD_REQUEST) + .entity("required query parameter \"owner\" is missing") + .build(); } final Optional runToDelete = context.storage.getRepairRun(runId); if (runToDelete.isPresent()) { if (runToDelete.get().getRunState() == RepairRun.RunState.RUNNING) { - return Response.status(Response.Status.FORBIDDEN).entity( - "Repair run with id \"" + runId - + "\" is currently running, and must be stopped before deleting").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Repair run with id \"" + runId + "\" is currently running, and must be stopped before deleting") + .build(); } if (!runToDelete.get().getOwner().equalsIgnoreCase(owner.get())) { - return Response.status(Response.Status.FORBIDDEN).entity( - "Repair run with id \"" + runId + "\" is not owned by the user you defined: " - + owner.get()).build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Repair run with id \"" + runId + "\" is not owned by the user you defined: " + owner.get()) + .build(); } if (context.storage.getSegmentAmountForRepairRunWithState(runId, RepairSegment.State.RUNNING) > 0) { - return Response.status(Response.Status.FORBIDDEN).entity( - "Repair run with id \"" + runId - + "\" has a running segment, which must be waited to finish before deleting").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity( + "Repair run with id \"" + + runId + + "\" has a running segment, which must be waited to finish before deleting") + .build(); } // Need to get the RepairUnit before it's possibly deleted. - final Optional unitPossiblyDeleted = - context.storage.getRepairUnit(runToDelete.get().getRepairUnitId()); - final int segmentsRepaired = - context.storage.getSegmentAmountForRepairRunWithState(runId, RepairSegment.State.DONE); + final Optional unitPossiblyDeleted + = context.storage.getRepairUnit(runToDelete.get().getRepairUnitId()); + + final int segmentsRepaired + = context.storage.getSegmentAmountForRepairRunWithState(runId, RepairSegment.State.DONE); + final Optional deletedRun = context.storage.deleteRepairRun(runId); if (deletedRun.isPresent()) { - final RepairRunStatus repairRunStatus = - new RepairRunStatus(deletedRun.get(), unitPossiblyDeleted.get(), segmentsRepaired); + final RepairRunStatus repairRunStatus + = new RepairRunStatus(deletedRun.get(), unitPossiblyDeleted.get(), segmentsRepaired); + return Response.ok().entity(repairRunStatus).build(); } } - return Response - .status(Response.Status.NOT_FOUND) - .entity("Repair run with id \"" + runId + "\" not found") - .build(); + try { + // safety clean, in case of zombie segments + context.storage.deleteRepairRun(runId); + } catch (RuntimeException ignore) { } + return Response.status(Response.Status.NOT_FOUND).entity("Repair run with id \"" + runId + "\" not found").build(); } private static void checkRepairParallelismString(String repairParallelism) throws ReaperException { try { RepairParallelism.valueOf(repairParallelism.toUpperCase()); - } catch (final java.lang.IllegalArgumentException ex) { + } catch (IllegalArgumentException ex) { throw new ReaperException( - "invalid repair parallelism given \"" + repairParallelism - + "\", must be one of: " + Arrays.toString(RepairParallelism.values()), ex); + "invalid repair parallelism given \"" + + repairParallelism + + "\", must be one of: " + + Arrays.toString(RepairParallelism.values()), + ex); } } } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java b/src/server/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java index 381b83e30..b4161322a 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java @@ -11,9 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources; -import static com.google.common.base.Preconditions.checkNotNull; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.view.RepairScheduleStatus; +import com.spotify.reaper.service.SchedulingManager; import java.net.MalformedURLException; import java.net.URI; @@ -24,7 +31,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; - import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -38,25 +44,19 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.view.RepairScheduleStatus; -import com.spotify.reaper.service.SchedulingManager; +import static com.google.common.base.Preconditions.checkNotNull; @Path("/repair_schedule") @Produces(MediaType.APPLICATION_JSON) -public class RepairScheduleResource { +public final class RepairScheduleResource { private static final Logger LOG = LoggerFactory.getLogger(RepairScheduleResource.class); @@ -67,12 +67,13 @@ public RepairScheduleResource(AppContext context) { } /** - * Endpoint used to create a repair schedule. Does not allow triggering the run. - * Repair schedule will create new repair runs based on the schedule. + * Endpoint used to create a repair schedule. Does not allow triggering the run. Repair schedule will create new + * repair runs based on the schedule. * - * Notice that query parameter "tables" can be a single String, or a - * comma-separated list of table names. If the "tables" parameter is omitted, and only the - * keyspace is defined, then created repair runs will target all the tables in the keyspace. + *

+ * Notice that query parameter "tables" can be a single String, or a comma-separated list of table names. If the + * "tables" parameter is omitted, and only the keyspace is defined, then created repair runs will target all the + * tables in the keyspace. * * @return created repair schedule data as JSON. */ @@ -90,81 +91,66 @@ public Response addRepairSchedule( @QueryParam("scheduleDaysBetween") Optional scheduleDaysBetween, @QueryParam("scheduleTriggerTime") Optional scheduleTriggerTime, @QueryParam("nodes") Optional nodesToRepairParam, - @QueryParam("datacenters") Optional datacentersToRepairParam - ) { - LOG.info("add repair schedule called with: clusterName = {}, keyspace = {}, tables = {}, " - + "owner = {}, segmentCount = {}, repairParallelism = {}, " - + "intensity = {}, incrementalRepair = {}, scheduleDaysBetween = {}, scheduleTriggerTime = {}", - clusterName, keyspace, tableNamesParam, owner, segmentCount, repairParallelism, - intensityStr, incrementalRepairStr, scheduleDaysBetween, scheduleTriggerTime); + @QueryParam("datacenters") Optional datacentersToRepairParam) { + + LOG.info( + "add repair schedule called with: clusterName = {}, keyspace = {}, tables = {}, " + + "owner = {}, segmentCount = {}, repairParallelism = {}, " + + "intensity = {}, incrementalRepair = {}, scheduleDaysBetween = {}, scheduleTriggerTime = {}", + clusterName, + keyspace, + tableNamesParam, + owner, + segmentCount, + repairParallelism, + intensityStr, + incrementalRepairStr, + scheduleDaysBetween, + scheduleTriggerTime); try { Response possibleFailResponse = RepairRunResource.checkRequestForAddRepair( - context, clusterName, keyspace, owner, segmentCount, repairParallelism, intensityStr, incrementalRepairStr, - nodesToRepairParam, datacentersToRepairParam); + context, + clusterName, + keyspace, + owner, + segmentCount, + repairParallelism, + intensityStr, + incrementalRepairStr, + nodesToRepairParam, + datacentersToRepairParam); + if (null != possibleFailResponse) { return possibleFailResponse; } DateTime nextActivation; - if (scheduleTriggerTime.isPresent()) { - try { - nextActivation = DateTime.parse(scheduleTriggerTime.get()); - } catch (IllegalArgumentException ex) { - LOG.info("cannot parse data string: {}", scheduleTriggerTime.get(), ex); - return Response.status(Response.Status.BAD_REQUEST).entity( - "invalid schedule_trigger_time").build(); + try { + nextActivation = getNextActivationTime(scheduleTriggerTime); + if (nextActivation.isBeforeNow()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("given schedule_trigger_time is in the past: " + CommonTools.dateTimeToIso8601(nextActivation)) + .build(); } - LOG.info("first schedule activation will be: {}", CommonTools.dateTimeToISO8601(nextActivation)); - } else { - nextActivation = DateTime.now().plusDays(1).withTimeAtStartOfDay(); - LOG.info("no schedule_trigger_time given, so setting first scheduling next night: {}", CommonTools.dateTimeToISO8601(nextActivation)); - } - if (nextActivation.isBeforeNow()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "given schedule_trigger_time is in the past: " + CommonTools.dateTimeToISO8601(nextActivation)).build(); - } - - if(!scheduleDaysBetween.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "missing required parameter: scheduleDaysBetween").build(); - } - - Double intensity; - if (intensityStr.isPresent()) { - intensity = Double.parseDouble(intensityStr.get()); - } else { - intensity = context.config.getRepairIntensity(); - LOG.debug("no intensity given, so using default value: {}", intensity); + } catch (IllegalArgumentException ex) { + LOG.info("cannot parse data string: {}", scheduleTriggerTime.get(), ex); + return Response.status(Response.Status.BAD_REQUEST).entity("invalid schedule_trigger_time").build(); } - Boolean incrementalRepair; - if (incrementalRepairStr.isPresent()) { - incrementalRepair = Boolean.parseBoolean(incrementalRepairStr.get()); - } else { - incrementalRepair = context.config.getIncrementalRepair(); - LOG.debug("no incremental repair given, so using default value: {}", incrementalRepair); + if (!scheduleDaysBetween.isPresent()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing required parameter: scheduleDaysBetween") + .build(); } - int segments = context.config.getSegmentCount(); - if (segmentCount.isPresent()) { - LOG.debug("using given segment count {} instead of configured value {}", - segmentCount.get(), context.config.getSegmentCount()); - segments = segmentCount.get(); - } - - int daysBetween = context.config.getScheduleDaysBetween(); - if (scheduleDaysBetween.isPresent()) { - LOG.debug("using given schedule days between {} instead of configured value {}", - scheduleDaysBetween.get(), context.config.getScheduleDaysBetween()); - daysBetween = scheduleDaysBetween.get(); - } + int segments = getSegmentCount(segmentCount); + int daysBetween = getDaysBetween(scheduleDaysBetween); Cluster cluster = context.storage.getCluster(Cluster.toSymbolicName(clusterName.get())).get(); Set tableNames; try { - tableNames = CommonTools.getTableNamesBasedOnParam(context, cluster, keyspace.get(), - tableNamesParam); + tableNames = CommonTools.getTableNamesBasedOnParam(context, cluster, keyspace.get(), tableNamesParam); } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); @@ -180,59 +166,134 @@ public Response addRepairSchedule( final Set datacentersToRepair; try { - datacentersToRepair = CommonTools.getDatacentersToRepairBasedOnParam(context, cluster, - datacentersToRepairParam); + datacentersToRepair = CommonTools + .getDatacentersToRepairBasedOnParam(context, cluster, datacentersToRepairParam); } catch (final IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } - RepairUnit theRepairUnit = - CommonTools.getNewOrExistingRepairUnit(context, cluster, keyspace.get(), tableNames, incrementalRepair, - nodesToRepair, datacentersToRepair); - - if(theRepairUnit.getIncrementalRepair() != incrementalRepair) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "A repair Schedule already exist for the same cluster/keyspace/table but with a different incremental repair value." - + "Requested value: " + incrementalRepair + " | Existing value: " + theRepairUnit.getIncrementalRepair()).build(); + Boolean incrementalRepair = isIncrementalRepair(incrementalRepairStr); + + RepairUnit theRepairUnit = CommonTools.getNewOrExistingRepairUnit( + context, + cluster, + keyspace.get(), + tableNames, + incrementalRepair, + nodesToRepair, + datacentersToRepair); + + if (theRepairUnit.getIncrementalRepair().booleanValue() != incrementalRepair) { + return Response.status(Response.Status.BAD_REQUEST) + .entity( + "A repair Schedule already exist for the same cluster/keyspace/table" + + " but with a different incremental repair value. Requested value: " + + incrementalRepair + + " | Existing value: " + + theRepairUnit.getIncrementalRepair()) + .build(); } RepairParallelism parallelism = context.config.getRepairParallelism(); if (repairParallelism.isPresent()) { - LOG.debug("using given repair parallelism {} instead of configured value {}", - repairParallelism.get(), context.config.getRepairParallelism()); + LOG.debug( + "using given repair parallelism {} instead of configured value {}", + repairParallelism.get(), + context.config.getRepairParallelism()); parallelism = RepairParallelism.valueOf(repairParallelism.get().toUpperCase()); } - if(!parallelism.equals(RepairParallelism.PARALLEL) && incrementalRepair) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "It is not possible to mix sequential repair and incremental repairs. parallelism " - + parallelism + " : incrementalRepair " + incrementalRepair).build(); + if (!parallelism.equals(RepairParallelism.PARALLEL) && incrementalRepair) { + return Response.status(Response.Status.BAD_REQUEST) + .entity( + "It is not possible to mix sequential repair and incremental repairs. parallelism " + + parallelism + + " : incrementalRepair " + + incrementalRepair) + .build(); } + + Double intensity = getIntensity(intensityStr); try { RepairSchedule newRepairSchedule = CommonTools.storeNewRepairSchedule( - context, cluster, theRepairUnit, daysBetween, nextActivation, owner.get(), - segments, parallelism, intensity); - - return Response.created(buildRepairScheduleURI(uriInfo, newRepairSchedule)) - .entity(new RepairScheduleStatus(newRepairSchedule, theRepairUnit)).build(); + context, + cluster, + theRepairUnit, + daysBetween, + nextActivation, + owner.get(), + segments, + parallelism, + intensity); + + return Response.created(buildRepairScheduleUri(uriInfo, newRepairSchedule)) + .entity(new RepairScheduleStatus(newRepairSchedule, theRepairUnit)) + .build(); } catch (ReaperException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.CONFLICT).entity(ex.getMessage()).build(); } - } catch (ReaperException e) { LOG.error(e.getMessage(), e); return Response.status(500).entity(e.getMessage()).build(); } } + private int getDaysBetween(Optional scheduleDaysBetween) { + int daysBetween = context.config.getScheduleDaysBetween(); + if (scheduleDaysBetween.isPresent()) { + LOG.debug( + "using given schedule days between {} instead of configured value {}", + scheduleDaysBetween.get(), + context.config.getScheduleDaysBetween()); + daysBetween = scheduleDaysBetween.get(); + } + return daysBetween; + } + + private int getSegmentCount(Optional segmentCount) { + int segments = context.config.getSegmentCount(); + if (segmentCount.isPresent()) { + LOG.debug( + "using given segment count {} instead of configured value {}", + segmentCount.get(), + context.config.getSegmentCount()); + segments = segmentCount.get(); + } + return segments; + } + + private Boolean isIncrementalRepair(Optional incrementalRepairStr) { + Boolean incrementalRepair; + if (incrementalRepairStr.isPresent()) { + incrementalRepair = Boolean.parseBoolean(incrementalRepairStr.get()); + } else { + incrementalRepair = context.config.getIncrementalRepair(); + LOG.debug("no incremental repair given, so using default value: {}", incrementalRepair); + } + return incrementalRepair; + } + + private Double getIntensity(Optional intensityStr) throws NumberFormatException { + Double intensity; + if (intensityStr.isPresent()) { + intensity = Double.parseDouble(intensityStr.get()); + } else { + intensity = context.config.getRepairIntensity(); + LOG.debug("no intensity given, so using default value: {}", intensity); + } + return intensity; + } + /** - * Modifies a state of the repair schedule.

Currently supports PAUSED -> ACTIVE and - * ACTIVE -> PAUSED. + * Modifies a state of the repair schedule. * - * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 400 - * (BAD_REQUEST) if transition is not supported. + *

+ * Currently supports PAUSED -> ACTIVE and ACTIVE -> PAUSED. + * + * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 400 (BAD_REQUEST) if + * transition is not supported. */ @PUT @Path("/{id}") @@ -241,26 +302,22 @@ public Response modifyState( @PathParam("id") UUID repairScheduleId, @QueryParam("state") Optional state) { - LOG.info("modify repair schedule state called with: id = {}, state = {}", - repairScheduleId, state); + LOG.info("modify repair schedule state called with: id = {}, state = {}", repairScheduleId, state); if (!state.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) - .entity("\"state\" argument missing").build(); + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).entity("\"state\" argument missing").build(); } Optional repairSchedule = context.storage.getRepairSchedule(repairScheduleId); if (!repairSchedule.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).entity("repair schedule with id " - + repairScheduleId + " not found") + return Response.status(Response.Status.NOT_FOUND) + .entity("repair schedule with id " + repairScheduleId + " not found") .build(); } - Optional repairUnit = - context.storage.getRepairUnit(repairSchedule.get().getRepairUnitId()); + Optional repairUnit = context.storage.getRepairUnit(repairSchedule.get().getRepairUnitId()); if (!repairUnit.isPresent()) { - String errMsg = - "repair unit with id " + repairSchedule.get().getRepairUnitId() + " not found"; + String errMsg = "repair unit with id " + repairSchedule.get().getRepairUnitId() + " not found"; LOG.error(errMsg); return Response.status(Response.Status.NOT_FOUND).entity(errMsg).build(); } @@ -271,15 +328,15 @@ public Response modifyState( } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) - .entity("invalid \"state\" argument: " + state.get()).build(); + .entity("invalid \"state\" argument: " + state.get()) + .build(); } RepairSchedule.State oldState = repairSchedule.get().getState(); if (oldState == newState) { - return Response - .status(Response.Status.NOT_MODIFIED) - .entity("given \"state\" is same as the current state") - .build(); + return Response.status(Response.Status.NOT_MODIFIED) + .entity("given \"state\" is same as the current state") + .build(); } if (isPausing(oldState, newState)) { @@ -287,8 +344,7 @@ public Response modifyState( } else if (isResuming(oldState, newState)) { return resumeSchedule(repairSchedule.get(), repairUnit.get()); } else { - String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(), - newState.toString()); + String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(), newState.toString()); LOG.error(errMsg); return Response.status(Response.Status.BAD_REQUEST).entity(errMsg).build(); } @@ -319,14 +375,14 @@ private Response resumeSchedule(RepairSchedule repairSchedule, RepairUnit repair */ @GET @Path("/{id}") - public Response getRepairSchedule(@PathParam("id") UUID repairScheduleId) { + public Response getRepairSchedule( + @PathParam("id") UUID repairScheduleId) { LOG.debug("get repair_schedule called with: id = {}", repairScheduleId); Optional repairSchedule = context.storage.getRepairSchedule(repairScheduleId); if (repairSchedule.isPresent()) { return Response.ok().entity(getRepairScheduleStatus(repairSchedule.get())).build(); } else { - return Response.status(404).entity( - "repair schedule with id " + repairScheduleId + " doesn't exist").build(); + return Response.status(404).entity("repair schedule with id " + repairScheduleId + " doesn't exist").build(); } } @@ -336,10 +392,10 @@ public Response getRepairSchedule(@PathParam("id") UUID repairScheduleId) { */ @GET @Path("/cluster/{cluster_name}") - public Response getRepairSchedulesForCluster(@PathParam("cluster_name") String clusterName) { + public Response getRepairSchedulesForCluster( + @PathParam("cluster_name") String clusterName) { LOG.debug("get repair schedules for cluster called with: cluster_name = {}", clusterName); - Collection repairSchedules = - context.storage.getRepairSchedulesForCluster(clusterName); + Collection repairSchedules = context.storage.getRepairSchedulesForCluster(clusterName); Collection repairScheduleViews = new ArrayList<>(); for (RepairSchedule repairSchedule : repairSchedules) { repairScheduleViews.add(getRepairScheduleStatus(repairSchedule)); @@ -351,10 +407,9 @@ public Response getRepairSchedulesForCluster(@PathParam("cluster_name") String c * @return RepairSchedule status for viewing */ private RepairScheduleStatus getRepairScheduleStatus(RepairSchedule repairSchedule) { - Optional repairUnit = - context.storage.getRepairUnit(repairSchedule.getRepairUnitId()); - Preconditions.checkState(repairUnit.isPresent(), "no repair unit found with id: " - + repairSchedule.getRepairUnitId()); + Optional repairUnit = context.storage.getRepairUnit(repairSchedule.getRepairUnitId()); + Preconditions.checkState( + repairUnit.isPresent(), "no repair unit found with id: " + repairSchedule.getRepairUnitId()); return new RepairScheduleStatus(repairSchedule, repairUnit.get()); } @@ -363,7 +418,7 @@ private RepairScheduleStatus getRepairScheduleStatus(RepairSchedule repairSchedu * * @return The created resource URI. */ - private URI buildRepairScheduleURI(UriInfo uriInfo, RepairSchedule repairSchedule) { + private URI buildRepairScheduleUri(UriInfo uriInfo, RepairSchedule repairSchedule) { String newRepairSchedulePathPart = "repair_schedule/" + repairSchedule.getId(); URI scheduleUri = null; try { @@ -376,14 +431,15 @@ private URI buildRepairScheduleURI(UriInfo uriInfo, RepairSchedule repairSchedul } /** - * @param clusterName The cluster name to list the schedules for. If not given, - * will list all schedules for all clusters. - * @param keyspaceName The keyspace name to list schedules for. Limits the returned list - * and works whether the cluster name is given or not. + * @param clusterName The cluster name to list the schedules for. If not given, will list all schedules for all + * clusters. + * @param keyspaceName The keyspace name to list schedules for. Limits the returned list and works whether the cluster + * name is given or not. * @return All schedules in the system. */ @GET - public Response listSchedules(@QueryParam("clusterName") Optional clusterName, + public Response listSchedules( + @QueryParam("clusterName") Optional clusterName, @QueryParam("keyspace") Optional keyspaceName) { List scheduleStatuses = Lists.newArrayList(); Collection schedules = getScheduleList(clusterName, keyspaceName); @@ -392,8 +448,7 @@ public Response listSchedules(@QueryParam("clusterName") Optional cluste if (unit.isPresent()) { scheduleStatuses.add(new RepairScheduleStatus(schedule, unit.get())); } else { - String errMsg = String.format( - "Found repair schedule %d with no associated repair unit", schedule.getId()); + String errMsg = String.format("Found repair schedule %s with no associated repair unit", schedule.getId()); LOG.error(errMsg); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } @@ -401,12 +456,10 @@ public Response listSchedules(@QueryParam("clusterName") Optional cluste return Response.status(Response.Status.OK).entity(scheduleStatuses).build(); } - private Collection getScheduleList(Optional clusterName, - Optional keyspaceName) { + private Collection getScheduleList(Optional clusterName, Optional keyspaceName) { Collection schedules; if (clusterName.isPresent() && keyspaceName.isPresent()) { - schedules = context.storage.getRepairSchedulesForClusterAndKeyspace(clusterName.get(), - keyspaceName.get()); + schedules = context.storage.getRepairSchedulesForClusterAndKeyspace(clusterName.get(), keyspaceName.get()); } else if (clusterName.isPresent()) { schedules = context.storage.getRepairSchedulesForCluster(clusterName.get()); } else if (keyspaceName.isPresent()) { @@ -420,50 +473,74 @@ private Collection getScheduleList(Optional clusterName, /** * Delete a RepairSchedule object with given id. * + *

* Repair schedule can only be deleted when it is not active, so you must stop it first. * * @param repairScheduleId The id for the RepairSchedule instance to delete. - * @param owner The assigned owner of the deleted resource. Must match the stored one. + * @param owner The assigned owner of the deleted resource. Must match the stored one. * @return The deleted RepairSchedule instance, with state overwritten to string "DELETED". */ @DELETE @Path("/{id}") - public Response deleteRepairSchedule(@PathParam("id") UUID repairScheduleId, + public Response deleteRepairSchedule( + @PathParam("id") UUID repairScheduleId, @QueryParam("owner") Optional owner) { - LOG.info("delete repair schedule called with repairScheduleId: {}, and owner: {}", - repairScheduleId, owner); + LOG.info("delete repair schedule called with repairScheduleId: {}, and owner: {}", repairScheduleId, owner); if (!owner.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST).entity( - "required query parameter \"owner\" is missing").build(); + return Response.status(Response.Status.BAD_REQUEST) + .entity("required query parameter \"owner\" is missing") + .build(); } Optional scheduleToDelete = context.storage.getRepairSchedule(repairScheduleId); if (scheduleToDelete.isPresent()) { - if (scheduleToDelete.get().getState() == RepairSchedule.State.ACTIVE) { - return Response.status(Response.Status.FORBIDDEN).entity( - "Repair schedule with id \"" + repairScheduleId - + "\" is currently running, and must be stopped before deleting").build(); - } - if (!scheduleToDelete.get().getOwner().equalsIgnoreCase(owner.get())) { - return Response.status(Response.Status.FORBIDDEN).entity( - "Repair schedule with id \"" + repairScheduleId - + "\" is not owned by the user you defined: " + owner.get()).build(); - } - // Need to get the RepairUnit before it's possibly deleted. - Optional possiblyDeletedUnit = - context.storage.getRepairUnit(scheduleToDelete.get().getRepairUnitId()); - Optional deletedSchedule = - context.storage.deleteRepairSchedule(repairScheduleId); - if (deletedSchedule.isPresent()) { - RepairScheduleStatus scheduleStatus = new RepairScheduleStatus(deletedSchedule.get(), - possiblyDeletedUnit.get()); - return Response.ok().entity(scheduleStatus).build(); - } + if (scheduleToDelete.get().getState() == RepairSchedule.State.ACTIVE) { + return Response.status(Response.Status.FORBIDDEN) + .entity( + "Repair schedule with id \"" + + repairScheduleId + + "\" is currently running, and must be stopped before deleting") + .build(); + } + if (!scheduleToDelete.get().getOwner().equalsIgnoreCase(owner.get())) { + return Response.status(Response.Status.FORBIDDEN) + .entity( + "Repair schedule with id \"" + + repairScheduleId + + "\" is not owned by the user you defined: " + + owner.get()) + .build(); + } + // Need to get the RepairUnit before it's possibly deleted. + Optional possiblyDeletedUnit + = context.storage.getRepairUnit(scheduleToDelete.get().getRepairUnitId()); + + Optional deletedSchedule + = context.storage.deleteRepairSchedule(repairScheduleId); + + if (deletedSchedule.isPresent()) { + return Response + .ok() + .entity(new RepairScheduleStatus(deletedSchedule.get(), possiblyDeletedUnit.get())) + .build(); + } } - return Response - .status(Response.Status.NOT_FOUND) - .entity("Repair schedule with id \"" + repairScheduleId + "\" not found") - .build(); + return Response.status(Response.Status.NOT_FOUND) + .entity("Repair schedule with id \"" + repairScheduleId + "\" not found") + .build(); } + private DateTime getNextActivationTime(Optional scheduleTriggerTime) throws IllegalArgumentException { + DateTime nextActivation; + if (scheduleTriggerTime.isPresent()) { + nextActivation = DateTime.parse(scheduleTriggerTime.get()); + LOG.info("first schedule activation will be: {}", CommonTools.dateTimeToIso8601(nextActivation)); + } else { + nextActivation = DateTime.now().plusDays(1).withTimeAtStartOfDay(); + LOG.info( + "no schedule_trigger_time given, so setting first scheduling next night: {}", + CommonTools.dateTimeToIso8601(nextActivation)); + } + return nextActivation; + } } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/package-info.java b/src/server/src/main/java/com/spotify/reaper/resources/package-info.java new file mode 100644 index 000000000..1a60ddf38 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/resources/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.resources; diff --git a/src/server/src/main/java/com/spotify/reaper/resources/view/ClusterStatus.java b/src/server/src/main/java/com/spotify/reaper/resources/view/ClusterStatus.java index eb3fdcd79..20c6fd352 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/view/ClusterStatus.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/view/ClusterStatus.java @@ -11,31 +11,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources.view; -import com.fasterxml.jackson.annotation.JsonProperty; import com.spotify.reaper.core.Cluster; -import jersey.repackaged.com.google.common.collect.Lists; - import java.util.Collection; import java.util.Set; -public class ClusterStatus { +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class ClusterStatus { @JsonProperty public final String name; + @JsonProperty("seed_hosts") public final Set seedHosts; + @JsonProperty("repair_runs") public final Collection repairRuns; + @JsonProperty("repair_schedules") public final Collection repairSchedules; + @JsonProperty("nodes_status") public final NodesStatus nodesStatus; - - public ClusterStatus(Cluster cluster, Collection repairRuns, - Collection repairSchedules, NodesStatus nodesStatus) { + + public ClusterStatus( + Cluster cluster, + Collection repairRuns, + Collection repairSchedules, + NodesStatus nodesStatus) { + this.name = cluster.getName(); this.seedHosts = cluster.getSeedHosts(); this.repairRuns = repairRuns; diff --git a/src/server/src/main/java/com/spotify/reaper/resources/view/NodesStatus.java b/src/server/src/main/java/com/spotify/reaper/resources/view/NodesStatus.java index 9240a227c..8903cfb8b 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/view/NodesStatus.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/view/NodesStatus.java @@ -11,26 +11,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources.view; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.base.Optional; -import com.google.common.collect.Sets; -import jersey.repackaged.com.google.common.collect.Lists; -import jersey.repackaged.com.google.common.collect.Maps; - -import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.regex.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; -public class NodesStatus { - @JsonProperty - public final List endpointStates; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Optional; +import com.google.common.collect.Sets; +import jersey.repackaged.com.google.common.collect.Lists; +import jersey.repackaged.com.google.common.collect.Maps; + +public final class NodesStatus { + private static final List ENDPOINT_NAME_PATTERNS = Lists.newArrayList(); private static final List ENDPOINT_STATUS_PATTERNS = Lists.newArrayList(); private static final List ENDPOINT_DC_PATTERNS = Lists.newArrayList(); @@ -41,8 +41,8 @@ public class NodesStatus { private static final List ENDPOINT_HOSTID_PATTERNS = Lists.newArrayList(); private static final List ENDPOINT_TOKENS_PATTERNS = Lists.newArrayList(); - private static final Pattern ENDPOINT_NAME_PATTERN = Pattern.compile("^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})", - Pattern.MULTILINE | Pattern.DOTALL); + private static final Pattern ENDPOINT_NAME_PATTERN + = Pattern.compile("^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})", Pattern.MULTILINE | Pattern.DOTALL); private static final Pattern ENDPOINT_STATUS_22_PATTERN = Pattern.compile("(STATUS):([0-9]*):(\\w+)"); private static final Pattern ENDPOINT_DC_22_PATTERN = Pattern.compile("(DC):([0-9]*):([0-9a-zA-Z-\\.]+)"); private static final Pattern ENDPOINT_RACK_22_PATTERN = Pattern.compile("(RACK):([0-9]*):([0-9a-zA-Z-\\.]+)"); @@ -61,6 +61,9 @@ public class NodesStatus { private static final String NOT_AVAILABLE = "Not available"; + @JsonProperty + public final List endpointStates; + static { initPatterns(); } @@ -74,7 +77,11 @@ public NodesStatus(String sourceNode, String allEndpointStates, Map simpleStates) { + private GossipInfo parseEndpointStatesString( + String sourceNode, + String allEndpointStates, + Map simpleStates) { + List endpointStates = Lists.newArrayList(); Set endpoints = Sets.newHashSet(); Matcher matcher; @@ -82,7 +89,7 @@ private GossipInfo parseEndpointStatesString(String sourceNode, String allEndpoi String[] strEndpoints = allEndpointStates.split("/"); Double totalLoad = 0.0; - for(int i=1;i status = Optional.absent(); Optional endpoint = parseEndpointState(ENDPOINT_NAME_PATTERNS, endpointString, 1, String.class); @@ -104,19 +111,28 @@ private GossipInfo parseEndpointStatesString(String sourceNode, String allEndpoi Optional load = parseEndpointState(ENDPOINT_LOAD_PATTERNS, endpointString, 3, Double.class); totalLoad += load.or(0.0); - EndpointState endpointState = new EndpointState(endpoint.or(NOT_AVAILABLE), hostId.or(NOT_AVAILABLE), dc.or(NOT_AVAILABLE), rack.or(NOT_AVAILABLE), status.or(NOT_AVAILABLE), severity.or(0.0), - releaseVersion.or(NOT_AVAILABLE), tokens.or(NOT_AVAILABLE), load.or(0.0)); + EndpointState endpointState = new EndpointState( + endpoint.or(NOT_AVAILABLE), + hostId.or(NOT_AVAILABLE), + dc.or(NOT_AVAILABLE), + rack.or(NOT_AVAILABLE), + status.or(NOT_AVAILABLE), + severity.or(0.0), + releaseVersion.or(NOT_AVAILABLE), + tokens.or(NOT_AVAILABLE), + load.or(0.0)); endpoints.add(endpoint.or(NOT_AVAILABLE)); endpointStates.add(endpointState); } Map>> endpointsByDcAndRack = Maps.newHashMap(); - Map> endpointsByDc = endpointStates.stream() - .collect(Collectors.groupingBy(EndpointState::getDc, Collectors.toList())); + Map> endpointsByDc + = endpointStates.stream().collect(Collectors.groupingBy(EndpointState::getDc, Collectors.toList())); - for(String dc:endpointsByDc.keySet()) { - Map> endpointsByRack = endpointsByDc.get(dc).stream().collect(Collectors.groupingBy(EndpointState::getRack, Collectors.toList())); + for (String dc : endpointsByDc.keySet()) { + Map> endpointsByRack + = endpointsByDc.get(dc).stream().collect(Collectors.groupingBy(EndpointState::getRack, Collectors.toList())); endpointsByDcAndRack.put(dc, endpointsByRack); } @@ -151,17 +167,26 @@ private static void initPatterns() { ENDPOINT_TOKENS_PATTERNS.add(ENDPOINT_TOKENS_22_PATTERN); } - public class GossipInfo { + public final class GossipInfo { + @JsonProperty public final String sourceNode; + @JsonProperty public final Map>> endpoints; + @JsonProperty public final Double totalLoad; + @JsonProperty public final Set endpointNames; - public GossipInfo(String sourceNode, Map>> endpoints, Double totalLoad, Set endpointNames) { + public GossipInfo( + String sourceNode, + Map>> endpoints, + Double totalLoad, + Set endpointNames) { + this.sourceNode = sourceNode; this.endpoints = endpoints; this.totalLoad = totalLoad; @@ -169,28 +194,46 @@ public GossipInfo(String sourceNode, Map } } - public class EndpointState { + public final class EndpointState { + @JsonProperty public final String endpoint; + @JsonProperty public final String hostId; + @JsonProperty public final String dc; + @JsonProperty public final String rack; + @JsonProperty public final String status; + @JsonProperty public final Double severity; + @JsonProperty public final String releaseVersion; + @JsonProperty public final String tokens; + @JsonProperty public final Double load; - public EndpointState(String endpoint, String hostId, String dc, String rack, String status, Double severity, - String releaseVersion, String tokens, Double load) { + public EndpointState( + String endpoint, + String hostId, + String dc, + String rack, + String status, + Double severity, + String releaseVersion, + String tokens, + Double load) { + this.endpoint = endpoint; this.hostId = hostId; this.dc = dc; @@ -200,27 +243,44 @@ public EndpointState(String endpoint, String hostId, String dc, String rack, Str this.releaseVersion = releaseVersion; this.tokens = tokens; this.load = load; - } - - public String getDc() { - return this.dc; - } - - public String getRack() { - return this.rack; - } - - @Override - public String toString() { - return "Endpoint : " + endpoint + " / " - + "Status : " + status + " / " - + "DC : " + dc + " / " - + "Rack : " + rack + " / " - + "Release version : " + releaseVersion + " / " - + "Load : " + load + " / " - + "Severity : " + severity + " / " - + "Host Id : " + hostId + " / " - + "Tokens : " + tokens; - } + } + + public String getDc() { + return this.dc; + } + + public String getRack() { + return this.rack; + } + + @Override + public String toString() { + return "Endpoint : " + + endpoint + + " / " + + "Status : " + + status + + " / " + + "DC : " + + dc + + " / " + + "Rack : " + + rack + + " / " + + "Release version : " + + releaseVersion + + " / " + + "Load : " + + load + + " / " + + "Severity : " + + severity + + " / " + + "Host Id : " + + hostId + + " / " + + "Tokens : " + + tokens; + } } } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java b/src/server/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java index fb997c84b..187d07761 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java @@ -11,27 +11,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources.view; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.CommonTools; + import java.util.Collection; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.cassandra.repair.RepairParallelism; import org.apache.commons.lang3.time.DurationFormatUtils; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.format.ISODateTimeFormat; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.CommonTools; - /** * Contains the data to be shown when querying repair run status. */ -public class RepairRunStatus { +public final class RepairRunStatus { @JsonProperty private String cause; @@ -102,11 +103,27 @@ public class RepairRunStatus { public RepairRunStatus() { } - public RepairRunStatus(UUID runId, String clusterName, String keyspaceName, - Collection columnFamilies, int segmentsRepaired, int totalSegments, - RepairRun.RunState state, DateTime startTime, DateTime endTime, String cause, String owner, - String lastEvent, DateTime creationTime, DateTime pauseTime, double intensity, boolean incrementalRepair, - RepairParallelism repairParallelism, Collection nodes, Collection datacenters) { + public RepairRunStatus( + UUID runId, + String clusterName, + String keyspaceName, + Collection columnFamilies, + int segmentsRepaired, + int totalSegments, + RepairRun.RunState state, + DateTime startTime, + DateTime endTime, + String cause, + String owner, + String lastEvent, + DateTime creationTime, + DateTime pauseTime, + double intensity, + boolean incrementalRepair, + RepairParallelism repairParallelism, + Collection nodes, + Collection datacenters) { + this.id = runId; this.cause = cause; this.owner = owner; @@ -131,18 +148,18 @@ public RepairRunStatus(UUID runId, String clusterName, String keyspaceName, duration = null; } else { duration = DurationFormatUtils.formatDurationWords( - new Duration(startTime.toInstant(), endTime.toInstant()).getMillis(), - true, false); + new Duration(startTime.toInstant(), endTime.toInstant()).getMillis(), true, false); } if (startTime == null || endTime != null) { estimatedTimeOfArrival = null; } else { - if (state == RepairRun.RunState.ERROR || state == RepairRun.RunState.DELETED || - state == RepairRun.RunState.ABORTED || segmentsRepaired == 0) { + if (state == RepairRun.RunState.ERROR + || state == RepairRun.RunState.DELETED + || state == RepairRun.RunState.ABORTED + || segmentsRepaired == 0) { estimatedTimeOfArrival = null; - } - else { + } else { long now = DateTime.now().getMillis(); long currentDuration = now - startTime.getMillis(); long millisecondsPerSegment = currentDuration / segmentsRepaired; @@ -170,53 +187,54 @@ public RepairRunStatus(RepairRun repairRun, RepairUnit repairUnit, int segmentsR repairRun.getPauseTime(), repairRun.getIntensity(), repairUnit.getIncrementalRepair(), - repairRun.getRepairParallelism(), repairUnit.getNodes(), repairUnit.getDatacenters() - ); + repairRun.getRepairParallelism(), + repairUnit.getNodes(), + repairUnit.getDatacenters()); } @JsonProperty("creation_time") - public String getCreationTimeISO8601() { - return CommonTools.dateTimeToISO8601(creationTime); + public String getCreationTimeIso8601() { + return CommonTools.dateTimeToIso8601(creationTime); } @JsonProperty("creation_time") - public void setCreationTimeISO8601(String dateStr) { + public void setCreationTimeIso8601(String dateStr) { if (null != dateStr) { creationTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } } @JsonProperty("start_time") - public String getStartTimeISO8601() { - return CommonTools.dateTimeToISO8601(startTime); + public String getStartTimeIso8601() { + return CommonTools.dateTimeToIso8601(startTime); } @JsonProperty("start_time") - public void setStartTimeISO8601(String dateStr) { + public void setStartTimeIso8601(String dateStr) { if (null != dateStr) { startTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } } @JsonProperty("end_time") - public String getEndTimeISO8601() { - return CommonTools.dateTimeToISO8601(endTime); + public String getEndTimeIso8601() { + return CommonTools.dateTimeToIso8601(endTime); } @JsonProperty("end_time") - public void setEndTimeISO8601(String dateStr) { + public void setEndTimeIso8601(String dateStr) { if (null != dateStr) { endTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } } @JsonProperty("pause_time") - public String getPauseTimeISO8601() { - return CommonTools.dateTimeToISO8601(pauseTime); + public String getPauseTimeIso8601() { + return CommonTools.dateTimeToIso8601(pauseTime); } @JsonProperty("pause_time") - public void setPauseTimeISO8601(String dateStr) { + public void setPauseTimeIso8601(String dateStr) { if (null != dateStr) { pauseTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } @@ -319,11 +337,11 @@ public void setIntensity(double intensity) { } public boolean getIncrementalRepair() { - return incrementalRepair; + return incrementalRepair; } public void setIncrementalRepair(boolean incrementalRepair) { - this.incrementalRepair = incrementalRepair; + this.incrementalRepair = incrementalRepair; } public int getTotalSegments() { @@ -367,12 +385,12 @@ public void setDuration(String duration) { } @JsonProperty("estimated_time_of_arrival") - public String getEstimatedTimeOfArrivalISO8601() { - return CommonTools.dateTimeToISO8601(estimatedTimeOfArrival); + public String getEstimatedTimeOfArrivalIso8601() { + return CommonTools.dateTimeToIso8601(estimatedTimeOfArrival); } @JsonProperty("estimated_time_of_arrival") - public void setEstimatedTimeOfArrivalISO8601(String dateStr) { + public void setEstimatedTimeOfArrivalIso8601(String dateStr) { if (null != dateStr) { estimatedTimeOfArrival = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java b/src/server/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java index 12385346d..35ca67e35 100644 --- a/src/server/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java +++ b/src/server/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java @@ -11,22 +11,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.resources.view; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import com.spotify.reaper.core.RepairSchedule; import com.spotify.reaper.core.RepairUnit; import com.spotify.reaper.resources.CommonTools; +import java.util.Collection; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; -import java.util.Collection; -import java.util.UUID; - -public class RepairScheduleStatus { +public final class RepairScheduleStatus { @JsonProperty private UUID id; @@ -82,11 +83,24 @@ public class RepairScheduleStatus { public RepairScheduleStatus() { } - public RepairScheduleStatus(UUID id, String owner, String clusterName, String keyspaceName, - Collection columnFamilies, RepairSchedule.State state, - DateTime creationTime, DateTime nextActivation, - DateTime pauseTime, double intensity, boolean incrementalRepair, int segmentCount, RepairParallelism repairParallelism, - int daysBetween, Collection nodes, Collection datacenters) { + public RepairScheduleStatus( + UUID id, + String owner, + String clusterName, + String keyspaceName, + Collection columnFamilies, + RepairSchedule.State state, + DateTime creationTime, + DateTime nextActivation, + DateTime pauseTime, + double intensity, + boolean incrementalRepair, + int segmentCount, + RepairParallelism repairParallelism, + int daysBetween, + Collection nodes, + Collection datacenters) { + this.id = id; this.owner = owner; this.clusterName = clusterName; @@ -120,8 +134,9 @@ public RepairScheduleStatus(RepairSchedule repairSchedule, RepairUnit repairUnit repairUnit.getIncrementalRepair(), repairSchedule.getSegmentCount(), repairSchedule.getRepairParallelism(), - repairSchedule.getDaysBetween(), repairUnit.getNodes(), repairUnit.getDatacenters() - ); + repairSchedule.getDaysBetween(), + repairUnit.getNodes(), + repairUnit.getDatacenters()); } public UUID getId() { @@ -209,12 +224,13 @@ public int getSegmentCount() { } public boolean getIncrementalRepair() { - return incrementalRepair; + return incrementalRepair; } public void setIncrementalRepair(boolean incrementalRepair) { - this.incrementalRepair = incrementalRepair; + this.incrementalRepair = incrementalRepair; } + public void setSegmentCount(int segmentCount) { this.segmentCount = segmentCount; } @@ -236,36 +252,36 @@ public void setDaysBetween(int daysBetween) { } @JsonProperty("creation_time") - public String getCreationTimeISO8601() { - return CommonTools.dateTimeToISO8601(creationTime); + public String getCreationTimeIso8601() { + return CommonTools.dateTimeToIso8601(creationTime); } @JsonProperty("creation_time") - public void setCreationTimeISO8601(String dateStr) { + public void setCreationTimeIso8601(String dateStr) { if (null != dateStr) { creationTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } } @JsonProperty("next_activation") - public String getNextActivationISO8601() { - return CommonTools.dateTimeToISO8601(nextActivation); + public String getNextActivationIso8601() { + return CommonTools.dateTimeToIso8601(nextActivation); } @JsonProperty("next_activation") - public void setNextActivationISO8601(String dateStr) { + public void setNextActivationIso8601(String dateStr) { if (null != dateStr) { nextActivation = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } } @JsonProperty("pause_time") - public String getPauseTimeISO8601() { - return CommonTools.dateTimeToISO8601(pauseTime); + public String getPauseTimeIso8601() { + return CommonTools.dateTimeToIso8601(pauseTime); } @JsonProperty("pause_time") - public void setPauseTimeISO8601(String dateStr) { + public void setPauseTimeIso8601(String dateStr) { if (null != dateStr) { pauseTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr); } @@ -290,5 +306,4 @@ public void setNodes(Collection nodes) { public void setDatacenters(Collection datacenters) { this.datacenters = datacenters; } - } diff --git a/src/server/src/main/java/com/spotify/reaper/resources/view/package-info.java b/src/server/src/main/java/com/spotify/reaper/resources/view/package-info.java new file mode 100644 index 000000000..5eb6f15df --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/resources/view/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.resources.view; diff --git a/src/server/src/main/java/com/spotify/reaper/service/AutoSchedulingManager.java b/src/server/src/main/java/com/spotify/reaper/service/AutoSchedulingManager.java index 81fd0e377..6503f28e6 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/AutoSchedulingManager.java +++ b/src/server/src/main/java/com/spotify/reaper/service/AutoSchedulingManager.java @@ -1,13 +1,31 @@ +/* + * 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.spotify.reaper.service; import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; import com.spotify.reaper.core.Cluster; + +import java.util.Collection; +import java.util.Timer; +import java.util.TimerTask; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; - -public class AutoSchedulingManager extends TimerTask { +public final class AutoSchedulingManager extends TimerTask { private static final Logger LOG = LoggerFactory.getLogger(AutoSchedulingManager.class); private static AutoSchedulingManager repairAutoSchedulingManager; @@ -23,24 +41,25 @@ public AutoSchedulingManager(AppContext context, ClusterRepairScheduler clusterR this.context = context; this.clusterRepairScheduler = clusterRepairScheduler; } - + public static synchronized void start(AppContext context) { if (null == repairAutoSchedulingManager) { - LOG.info("Starting new {} instance. First check in {}ms. Subsequent polls every {}ms", + LOG.info( + "Starting new {} instance. First check in {}ms. Subsequent polls every {}ms", AutoSchedulingManager.class.getSimpleName(), context.config.getAutoScheduling().getInitialDelayPeriod().toMillis(), - context.config.getAutoScheduling().getPeriodBetweenPolls().toMillis() - ); + context.config.getAutoScheduling().getPeriodBetweenPolls().toMillis()); repairAutoSchedulingManager = new AutoSchedulingManager(context); Timer timer = new Timer("AutoSchedulingManagerTimer"); timer.schedule( repairAutoSchedulingManager, context.config.getAutoScheduling().getInitialDelayPeriod().toMillis(), - context.config.getAutoScheduling().getPeriodBetweenPolls().toMillis() - ); + context.config.getAutoScheduling().getPeriodBetweenPolls().toMillis()); } else { - LOG.warn("there is already one instance of {} running, not starting new one", AutoSchedulingManager.class.getSimpleName()); + LOG.warn( + "there is already one instance of {} running, not starting new one", + AutoSchedulingManager.class.getSimpleName()); } } @@ -51,10 +70,9 @@ public void run() { for (Cluster cluster : clusters) { try { clusterRepairScheduler.scheduleRepairs(cluster); - } catch (Exception e) { + } catch (ReaperException | RuntimeException e) { LOG.error("Error while scheduling repairs for cluster {}", cluster, e); } } } - } diff --git a/src/server/src/main/java/com/spotify/reaper/service/ClusterRepairScheduler.java b/src/server/src/main/java/com/spotify/reaper/service/ClusterRepairScheduler.java index a4f79614b..80f525c02 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/ClusterRepairScheduler.java +++ b/src/server/src/main/java/com/spotify/reaper/service/ClusterRepairScheduler.java @@ -1,6 +1,26 @@ +/* + * 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.spotify.reaper.service; -import static java.lang.String.format; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.CommonTools; import java.util.Collection; import java.util.Collections; @@ -9,23 +29,18 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.CommonTools; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.String.format; + +public final class ClusterRepairScheduler { -public class ClusterRepairScheduler { private static final Logger LOG = LoggerFactory.getLogger(ClusterRepairScheduler.class); private static final String REPAIR_OWNER = "auto-scheduling"; private static final String SYSTEM_KEYSPACE_PREFIX = "system"; @@ -39,35 +54,48 @@ public void scheduleRepairs(Cluster cluster) throws ReaperException { AtomicInteger scheduleIndex = new AtomicInteger(); ScheduledRepairDiffView schedulesDiff = ScheduledRepairDiffView.compareWithExistingSchedules(context, cluster); schedulesDiff.keyspacesDeleted().forEach(keyspace -> deleteRepairSchedule(cluster, keyspace)); - schedulesDiff.keyspacesWithoutSchedules() + schedulesDiff + .keyspacesWithoutSchedules() .stream() .filter(keyspace -> keyspaceCandidateForRepair(cluster, keyspace)) - .forEach(keyspace -> createRepairSchedule(cluster, keyspace, nextActivationStartDate(scheduleIndex.getAndIncrement()))); + .forEach( + keyspace + -> createRepairSchedule(cluster, keyspace, nextActivationStartDate(scheduleIndex.getAndIncrement()))); } private DateTime nextActivationStartDate(int scheduleIndex) { - DateTime timeBeforeFirstSchedule = DateTime.now().plus(context.config.getAutoScheduling().getTimeBeforeFirstSchedule().toMillis()); + DateTime timeBeforeFirstSchedule + = DateTime.now().plus(context.config.getAutoScheduling().getTimeBeforeFirstSchedule().toMillis()); + if (context.config.getAutoScheduling().hasScheduleSpreadPeriod()) { - return timeBeforeFirstSchedule.plus(scheduleIndex * context.config.getAutoScheduling().getScheduleSpreadPeriod().toMillis()); + return timeBeforeFirstSchedule.plus( + scheduleIndex * context.config.getAutoScheduling().getScheduleSpreadPeriod().toMillis()); } return timeBeforeFirstSchedule; } private void deleteRepairSchedule(Cluster cluster, String keyspace) { - Collection scheduleCollection = context.storage.getRepairSchedulesForClusterAndKeyspace(cluster.getName(), keyspace); - scheduleCollection.forEach(repairSchedule -> { - context.storage.deleteRepairSchedule(repairSchedule.getId()); - LOG.info("Scheduled repair deleted: {}", repairSchedule); - }); + Collection scheduleCollection + = context.storage.getRepairSchedulesForClusterAndKeyspace(cluster.getName(), keyspace); + + scheduleCollection.forEach( + repairSchedule -> { + context.storage.deleteRepairSchedule(repairSchedule.getId()); + LOG.info("Scheduled repair deleted: {}", repairSchedule); + }); } private boolean keyspaceCandidateForRepair(Cluster cluster, String keyspace) { - if (keyspace.toLowerCase().startsWith(ClusterRepairScheduler.SYSTEM_KEYSPACE_PREFIX) || context.config.getAutoScheduling().getExcludedKeyspaces().contains(keyspace)) { + if (keyspace.toLowerCase().startsWith(ClusterRepairScheduler.SYSTEM_KEYSPACE_PREFIX) + || context.config.getAutoScheduling().getExcludedKeyspaces().contains(keyspace)) { LOG.debug("Scheduled repair skipped for system keyspace {} in cluster {}.", keyspace, cluster.getName()); return false; } if (keyspaceHasNoTable(context, cluster, keyspace)) { - LOG.warn("No tables found for keyspace {} in cluster {}. No repair will be scheduled for this keyspace.", keyspace, cluster.getName()); + LOG.warn( + "No tables found for keyspace {} in cluster {}. No repair will be scheduled for this keyspace.", + keyspace, + cluster.getName()); return false; } return true; @@ -79,23 +107,30 @@ private void createRepairSchedule(Cluster cluster, String keyspace, DateTime nex RepairSchedule repairSchedule = CommonTools.storeNewRepairSchedule( context, cluster, - CommonTools.getNewOrExistingRepairUnit(context, cluster, keyspace, Collections.emptySet(), incrementalRepair, - Collections.emptySet(), Collections.emptySet()), + CommonTools.getNewOrExistingRepairUnit( + context, + cluster, + keyspace, + Collections.emptySet(), + incrementalRepair, + Collections.emptySet(), + Collections.emptySet()), context.config.getScheduleDaysBetween(), nextActivationTime, REPAIR_OWNER, context.config.getSegmentCount(), context.config.getRepairParallelism(), - context.config.getRepairIntensity() - ); + context.config.getRepairIntensity()); LOG.info("Scheduled repair created: {}", repairSchedule); } catch (ReaperException e) { - Throwables.propagate(e); + throw Throwables.propagate(e); } } - private boolean keyspaceHasNoTable(AppContext context, Cluster cluster, String keyspace) { - try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { + private boolean keyspaceHasNoTable(AppContext context, Cluster cluster, String keyspace) { + try (JmxProxy jmxProxy + = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { + Set tables = jmxProxy.getTableNamesForKeyspace(keyspace); return tables.isEmpty(); } catch (ReaperException e) { @@ -104,38 +139,49 @@ private boolean keyspaceHasNoTable(AppContext context, Cluster cluster, String k } private static class ScheduledRepairDiffView { + private final ImmutableSet keyspacesThatRequireSchedules; private final ImmutableSet keyspacesDeleted; - public ScheduledRepairDiffView(AppContext context, Cluster cluster) throws ReaperException { + ScheduledRepairDiffView(AppContext context, Cluster cluster) throws ReaperException { Set allKeyspacesInCluster = keyspacesInCluster(context, cluster); Set keyspacesThatHaveSchedules = keyspacesThatHaveSchedules(context, cluster); - keyspacesThatRequireSchedules = Sets.difference(allKeyspacesInCluster, keyspacesThatHaveSchedules).immutableCopy(); + + keyspacesThatRequireSchedules + = Sets.difference(allKeyspacesInCluster, keyspacesThatHaveSchedules).immutableCopy(); + keyspacesDeleted = Sets.difference(keyspacesThatHaveSchedules, allKeyspacesInCluster).immutableCopy(); } - public static ScheduledRepairDiffView compareWithExistingSchedules(AppContext context, Cluster cluster) throws ReaperException { + static ScheduledRepairDiffView compareWithExistingSchedules(AppContext context, Cluster cluster) + throws ReaperException { + return new ScheduledRepairDiffView(context, cluster); } - public Set keyspacesWithoutSchedules() { + Set keyspacesWithoutSchedules() { return keyspacesThatRequireSchedules; } - public Set keyspacesDeleted() { + Set keyspacesDeleted() { return keyspacesDeleted; } private Set keyspacesThatHaveSchedules(AppContext context, Cluster cluster) { Collection currentSchedules = context.storage.getRepairSchedulesForCluster(cluster.getName()); - return currentSchedules.stream().map(repairSchedule -> { - Optional repairUnit = context.storage.getRepairUnit(repairSchedule.getRepairUnitId()); - return repairUnit.get().getKeyspaceName(); - }).collect(Collectors.toSet()); + return currentSchedules + .stream() + .map( + repairSchedule -> { + Optional repairUnit = context.storage.getRepairUnit(repairSchedule.getRepairUnitId()); + return repairUnit.get().getKeyspaceName(); + }) + .collect(Collectors.toSet()); } private Set keyspacesInCluster(AppContext context, Cluster cluster) throws ReaperException { - try (JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmxProxy + = context.jmxConnectionFactory.connectAny(cluster, context.config.getJmxConnectionTimeoutInSeconds())) { List keyspaces = jmxProxy.getKeyspaces(); if (keyspaces.isEmpty()) { String message = format("No keyspace found in cluster %s", cluster.getName()); diff --git a/src/server/src/main/java/com/spotify/reaper/service/RepairManager.java b/src/server/src/main/java/com/spotify/reaper/service/RepairManager.java index 562b4a213..486361118 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/RepairManager.java +++ b/src/server/src/main/java/com/spotify/reaper/service/RepairManager.java @@ -1,15 +1,33 @@ +/* + * 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.spotify.reaper.service; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.storage.IDistributedStorage; + import java.util.Collection; import java.util.Map; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.apache.cassandra.concurrent.NamedThreadFactory; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; @@ -19,35 +37,39 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.storage.IDistributedStorage; +import org.apache.cassandra.concurrent.NamedThreadFactory; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class RepairManager { private static final Logger LOG = LoggerFactory.getLogger(RepairManager.class); + // Caching all active RepairRunners. + @VisibleForTesting + public Map repairRunners = Maps.newConcurrentMap(); + private ListeningScheduledExecutorService executor; private long repairTimeoutMillis; private long retryDelayMillis; + + public long getRepairTimeoutMillis() { return repairTimeoutMillis; } - // Caching all active RepairRunners. - @VisibleForTesting - public Map repairRunners = Maps.newConcurrentMap(); - - public void initializeThreadPool(int threadAmount, long repairTimeout, - TimeUnit repairTimeoutTimeUnit, long retryDelay, + public void initializeThreadPool( + int threadAmount, + long repairTimeout, + TimeUnit repairTimeoutTimeUnit, + long retryDelay, TimeUnit retryDelayTimeUnit) { - executor = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(threadAmount, - new NamedThreadFactory("RepairRunner"))); + + executor = MoreExecutors.listeningDecorator( + Executors.newScheduledThreadPool(threadAmount, new NamedThreadFactory("RepairRunner"))); + repairTimeoutMillis = repairTimeoutTimeUnit.toMillis(repairTimeout); retryDelayMillis = retryDelayTimeUnit.toMillis(retryDelay); } @@ -56,7 +78,6 @@ public void initializeThreadPool(int threadAmount, long repairTimeout, * Consult storage to see if any repairs are running, and resume those repair runs. * * @param context Reaper's application context. - * @throws ReaperException */ public void resumeRunningRepairRuns(AppContext context) throws ReaperException { heartbeat(context); @@ -64,7 +85,7 @@ public void resumeRunningRepairRuns(AppContext context) throws ReaperException { for (RepairRun repairRun : running) { if (!repairRunners.containsKey(repairRun.getId())) { Collection runningSegments - = context.storage.getSegmentsWithState(repairRun.getId(), RepairSegment.State.RUNNING); + = context.storage.getSegmentsWithState(repairRun.getId(), RepairSegment.State.RUNNING); abortSegments(runningSegments, context, repairRun); LOG.info("Restarting run id {} that has no runner", repairRun.getId()); @@ -77,46 +98,53 @@ public void resumeRunningRepairRuns(AppContext context) throws ReaperException { if (repairRunners.containsKey(pausedRepairRun.getId())) { // Abort all running segments for paused repair runs Collection runningSegments - = context.storage.getSegmentsWithState(pausedRepairRun.getId(), RepairSegment.State.RUNNING); + = context.storage.getSegmentsWithState(pausedRepairRun.getId(), RepairSegment.State.RUNNING); abortSegments(runningSegments, context, pausedRepairRun); } - if(!repairRunners.containsKey(pausedRepairRun.getId())) { + if (!repairRunners.containsKey(pausedRepairRun.getId())) { startRunner(context, pausedRepairRun.getId()); } } } - private static void abortSegments(Collection runningSegments, AppContext context, RepairRun repairRun) { + private static void abortSegments( + Collection runningSegments, + AppContext context, + RepairRun repairRun) { + RepairUnit repairUnit = context.storage.getRepairUnit(repairRun.getRepairUnitId()).get(); for (RepairSegment segment : runningSegments) { - UUID leaderElectionId = repairUnit.getIncrementalRepair() ? repairRun.getId() : segment.getId(); - if (takeLead(context, leaderElectionId) || renewLead(context, leaderElectionId)) { - try (JmxProxy jmxProxy = context.jmxConnectionFactory - .connect(segment.getCoordinatorHost(), context.config.getJmxConnectionTimeoutInSeconds())) { - - SegmentRunner.abort(context, segment, jmxProxy); - } catch (ReaperException e) { - LOG.debug("Tried to abort repair on segment {} marked as RUNNING, but the host was down (so abortion won't be needed)", - segment.getId(), e); - } finally { - // if someone else does hold the lease, ie renewLead(..) was true, - // then their writes to repair_run table and any call to releaseLead(..) will throw an exception - releaseLead(context, leaderElectionId); - } + UUID leaderElectionId = repairUnit.getIncrementalRepair() ? repairRun.getId() : segment.getId(); + if (takeLead(context, leaderElectionId) || renewLead(context, leaderElectionId)) { + try (JmxProxy jmxProxy = context.jmxConnectionFactory.connect( + segment.getCoordinatorHost(), context.config.getJmxConnectionTimeoutInSeconds())) { + + SegmentRunner.abort(context, segment, jmxProxy); + } catch (ReaperException e) { + LOG.debug( + "Tried to abort repair on segment {} marked as RUNNING, " + + "but the host was down (so abortion won't be needed)", + segment.getId(), + e); + } finally { + // if someone else does hold the lease, ie renewLead(..) was true, + // then their writes to repair_run table and any call to releaseLead(..) will throw an exception + releaseLead(context, leaderElectionId); } + } } } public RepairRun startRepairRun(AppContext context, RepairRun runToBeStarted) throws ReaperException { assert null != executor : "you need to initialize the thread pool first"; UUID runId = runToBeStarted.getId(); - LOG.info("Starting a run with id #{} with current state '{}'", - runId, runToBeStarted.getRunState()); + LOG.info("Starting a run with id #{} with current state '{}'", runId, runToBeStarted.getRunState()); switch (runToBeStarted.getRunState()) { case NOT_STARTED: { - RepairRun updatedRun = runToBeStarted.with() + RepairRun updatedRun = runToBeStarted + .with() .runState(RepairRun.RunState.RUNNING) .startTime(DateTime.now()) .build(runToBeStarted.getId()); @@ -131,22 +159,21 @@ public RepairRun startRepairRun(AppContext context, RepairRun runToBeStarted) th .runState(RepairRun.RunState.RUNNING) .pauseTime(null) .build(runToBeStarted.getId()); + if (!context.storage.updateRepairRun(updatedRun)) { throw new ReaperException("failed updating repair run " + updatedRun.getId()); } return updatedRun; } case RUNNING: - Preconditions.checkState(!repairRunners.containsKey(runId), - "trying to re-trigger run that is already running, with id " + runId); + Preconditions.checkState( + !repairRunners.containsKey(runId), "trying to re-trigger run that is already running, with id " + runId); LOG.info("re-trigger a running run after restart, with id {}", runId); startRunner(context, runId); return runToBeStarted; case ERROR: { - RepairRun updatedRun = runToBeStarted.with() - .runState(RepairRun.RunState.RUNNING) - .endTime(null) - .build(runToBeStarted.getId()); + RepairRun updatedRun + = runToBeStarted.with().runState(RepairRun.RunState.RUNNING).endTime(null).build(runToBeStarted.getId()); if (!context.storage.updateRepairRun(updatedRun)) { throw new ReaperException("failed updating repair run " + updatedRun.getId()); } @@ -171,7 +198,8 @@ private void startRunner(AppContext context, UUID runId) { } else { LOG.error( "there is already a repair runner for run with id {}, so not starting new runner. This " - + "should not happen.", runId); + + "should not happen.", + runId); } } @@ -180,6 +208,7 @@ public RepairRun pauseRepairRun(AppContext context, RepairRun runToBePaused) thr .runState(RepairRun.RunState.PAUSED) .pauseTime(DateTime.now()) .build(runToBePaused.getId()); + if (!context.storage.updateRepairRun(updatedRun)) { throw new ReaperException("failed updating repair run " + updatedRun.getId()); } @@ -187,10 +216,12 @@ public RepairRun pauseRepairRun(AppContext context, RepairRun runToBePaused) thr } public RepairRun abortRepairRun(AppContext context, RepairRun runToBeAborted) throws ReaperException { - RepairRun updatedRun = runToBeAborted.with() + RepairRun updatedRun = runToBeAborted + .with() .runState(RepairRun.RunState.ABORTED) .pauseTime(DateTime.now()) .build(runToBeAborted.getId()); + if (!context.storage.updateRepairRun(updatedRun)) { throw new ReaperException("failed updating repair run " + updatedRun.getId()); } @@ -210,44 +241,47 @@ public void removeRunner(RepairRunner runner) { } private static void heartbeat(AppContext context) { - if (context.storage instanceof IDistributedStorage) { - ((IDistributedStorage)context.storage).saveHeartbeat(); - } + if (context.storage instanceof IDistributedStorage) { + ((IDistributedStorage) context.storage).saveHeartbeat(); + } } private static boolean takeLead(AppContext context, UUID leaderElectionId) { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "takeLead")).time()) { + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "takeLead")).time()) { - boolean result = context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).takeLead(leaderElectionId) - : true; + boolean result = context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).takeLead(leaderElectionId) + : true; - if (!result) { - context.metricRegistry.counter(MetricRegistry.name(RepairManager.class, "takeLead", "failed")).inc(); - } - return result; + if (!result) { + context.metricRegistry.counter(MetricRegistry.name(RepairManager.class, "takeLead", "failed")).inc(); + } + return result; } } private static boolean renewLead(AppContext context, UUID leaderElectionId) { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "renewLead")).time()) { + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "renewLead")).time()) { - boolean result = context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).renewLead(leaderElectionId) - : true; + boolean result = context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).renewLead(leaderElectionId) + : true; - if (!result) { - context.metricRegistry.counter(MetricRegistry.name(RepairManager.class, "renewLead", "failed")).inc(); - } - return result; + if (!result) { + context.metricRegistry.counter(MetricRegistry.name(RepairManager.class, "renewLead", "failed")).inc(); + } + return result; } } private static void releaseLead(AppContext context, UUID leaderElectionId) { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "releaseLead")).time()) { - if (context.storage instanceof IDistributedStorage) { - ((IDistributedStorage)context.storage).releaseLead(leaderElectionId); - } + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(RepairManager.class, "releaseLead")).time()) { + if (context.storage instanceof IDistributedStorage) { + ((IDistributedStorage) context.storage).releaseLead(leaderElectionId); + } } } } diff --git a/src/server/src/main/java/com/spotify/reaper/service/RepairParameters.java b/src/server/src/main/java/com/spotify/reaper/service/RepairParameters.java index 17d884f03..298ea5ed0 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/RepairParameters.java +++ b/src/server/src/main/java/com/spotify/reaper/service/RepairParameters.java @@ -11,23 +11,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.service; -import org.apache.cassandra.repair.RepairParallelism; import java.util.Set; +import org.apache.cassandra.repair.RepairParallelism; + /** * Represents the parameters of a repair command given to Cassandra. */ -public class RepairParameters { +public final class RepairParameters { + public final RingRange tokenRange; public final String keyspaceName; public final Set columnFamilies; public final RepairParallelism repairParallelism; - public RepairParameters(RingRange tokenRange, String keyspaceName, Set columnFamilies, + public RepairParameters( + RingRange tokenRange, + String keyspaceName, + Set columnFamilies, RepairParallelism repairParallelism) { + this.tokenRange = tokenRange; this.keyspaceName = keyspaceName; this.columnFamilies = columnFamilies; diff --git a/src/server/src/main/java/com/spotify/reaper/service/RepairRunner.java b/src/server/src/main/java/com/spotify/reaper/service/RepairRunner.java index 9103c1c7a..657093d6f 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/RepairRunner.java +++ b/src/server/src/main/java/com/spotify/reaper/service/RepairRunner.java @@ -11,43 +11,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.service; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.storage.IDistributedStorage; + import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicReferenceArray; - -import org.apache.cassandra.repair.RepairParallelism; -import org.apache.commons.lang3.tuple.Pair; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplication; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.storage.IDistributedStorage; -import java.util.Collection; -import java.util.UUID; -import java.util.stream.Collectors; +import org.apache.cassandra.repair.RepairParallelism; +import org.apache.commons.lang3.tuple.Pair; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class RepairRunner implements Runnable { +public final class RepairRunner implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(RepairRunner.class); @@ -58,29 +57,30 @@ public class RepairRunner implements Runnable { private final AtomicReferenceArray currentlyRunningSegments; private final List parallelRanges; - public RepairRunner(AppContext context, UUID repairRunId) - throws ReaperException { + public RepairRunner(AppContext context, UUID repairRunId) throws ReaperException { LOG.debug("Creating RepairRunner for run with ID {}", repairRunId); this.context = context; this.repairRunId = repairRunId; Optional repairRun = context.storage.getRepairRun(repairRunId); assert repairRun.isPresent() : "No RepairRun with ID " + repairRunId + " found from storage"; Optional cluster = context.storage.getCluster(repairRun.get().getClusterName()); - assert cluster.isPresent() : "No Cluster with name " + repairRun.get().getClusterName() - + " found from storage"; - Optional repairUnitOpt = - context.storage.getRepairUnit(repairRun.get().getRepairUnitId()); + assert cluster.isPresent() : "No Cluster with name " + repairRun.get().getClusterName() + " found from storage"; + Optional repairUnitOpt = context.storage.getRepairUnit(repairRun.get().getRepairUnitId()); assert repairUnitOpt.isPresent() : "No RepairUnit with id " + repairRun.get().getRepairUnitId() - + " found in storage"; + + " found in storage"; this.clusterName = cluster.get().getName(); - JmxProxy jmx = this.context.jmxConnectionFactory.connectAny(cluster.get(), context.config.getJmxConnectionTimeoutInSeconds()); + + JmxProxy jmx = this.context.jmxConnectionFactory + .connectAny(cluster.get(), context.config.getJmxConnectionTimeoutInSeconds()); String keyspace = repairUnitOpt.get().getKeyspaceName(); - int parallelRepairs = getPossibleParallelRepairsCount(jmx.getRangeToEndpointMap(keyspace), jmx.getEndpointToHostId()); - if((repairUnitOpt.isPresent() && repairUnitOpt.get().getIncrementalRepair()) || context.config.getLocalJmxMode()) { - // with incremental repair, can't have more parallel repairs than nodes + int parallelRepairs + = getPossibleParallelRepairsCount(jmx.getRangeToEndpointMap(keyspace), jmx.getEndpointToHostId()); + + if ((repairUnitOpt.isPresent() && repairUnitOpt.get().getIncrementalRepair()) || context.config.getLocalJmxMode()) { + // with incremental repair, can't have more parallel repairs than nodes // Same goes for local mode - parallelRepairs = 1; + parallelRepairs = 1; } currentlyRunningSegments = new AtomicReferenceArray(parallelRepairs); for (int i = 0; i < parallelRepairs; i++) { @@ -88,14 +88,14 @@ public RepairRunner(AppContext context, UUID repairRunId) } List ranges = jmx.getRangesForLocalEndpoint(keyspace); - Collection repairSegments - = context.config.getLocalJmxMode() && context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).getRepairSegmentsForRunInLocalMode(repairRunId, ranges) - : context.storage.getRepairSegmentsForRun(repairRunId); + Collection repairSegments = context.config.getLocalJmxMode() + && context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).getRepairSegmentsForRunInLocalMode(repairRunId, ranges) + : context.storage.getRepairSegmentsForRun(repairRunId); parallelRanges = getParallelRanges( - parallelRepairs, - Lists.newArrayList(Collections2.transform(repairSegments, segment -> segment.getTokenRange()))); + parallelRepairs, + Lists.newArrayList(Collections2.transform(repairSegments, segment -> segment.getTokenRange()))); } public UUID getRepairRunId() { @@ -103,39 +103,44 @@ public UUID getRepairRunId() { } @VisibleForTesting - public static int getPossibleParallelRepairsCount(Map, List> ranges, Map hostsInRing) - throws ReaperException { + public static int getPossibleParallelRepairsCount( + Map, List> ranges, + Map hostsInRing) throws ReaperException { + if (ranges.isEmpty()) { String msg = "Repairing 0-sized cluster."; LOG.error(msg); throw new ReaperException(msg); } - return Math.min(ranges.size() / ranges.values().iterator().next().size(), Math.max(1, hostsInRing.keySet().size()/ranges.values().iterator().next().size())); + return Math.min( + ranges.size() / ranges.values().iterator().next().size(), + Math.max(1, hostsInRing.keySet().size() / ranges.values().iterator().next().size())); } @VisibleForTesting public static List getParallelRanges(int parallelRepairs, List segments) throws ReaperException { + if (parallelRepairs == 0) { String msg = "Can't repair anything with 0 threads"; LOG.error(msg); throw new ReaperException(msg); } - Collections.sort(segments, RingRange.startComparator); + Collections.sort(segments, RingRange.START_COMPARATOR); List parallelRanges = Lists.newArrayList(); for (int i = 0; i < parallelRepairs - 1; i++) { - parallelRanges.add(new RingRange( - segments.get(i * segments.size() / parallelRepairs).getStart(), - segments.get((i + 1) * segments.size() / parallelRepairs).getStart() - )); + parallelRanges.add( + new RingRange( + segments.get(i * segments.size() / parallelRepairs).getStart(), + segments.get((i + 1) * segments.size() / parallelRepairs).getStart())); } - parallelRanges.add(new RingRange( - segments.get((parallelRepairs - 1) * segments.size() / parallelRepairs).getStart(), - segments.get(0).getStart() - )); + parallelRanges.add( + new RingRange( + segments.get((parallelRepairs - 1) * segments.size() / parallelRepairs).getStart(), + segments.get(0).getStart())); return parallelRanges; } @@ -145,7 +150,6 @@ public static List getParallelRanges(int parallelRepairs, List nextRepairSegment - = context.storage.getNextFreeSegmentInRange(repairRunId, Optional.of(parallelRanges.get(rangeIndex))); + = context.storage.getNextFreeSegmentInRange(repairRunId, Optional.of(parallelRanges.get(rangeIndex))); if (!nextRepairSegment.isPresent()) { LOG.debug("No repair segment available for range {}", parallelRanges.get(rangeIndex)); } else { - LOG.info("Next segment to run : {}", nextRepairSegment.get().getId()); + LOG.info("Next segment to run : {}", nextRepairSegment.get().getId()); UUID segmentId = nextRepairSegment.get().getId(); boolean wasSet = currentlyRunningSegments.compareAndSet(rangeIndex, null, segmentId); if (!wasSet) { - LOG.debug("Didn't set segment id `{}` to slot {} because it was busy", - segmentId, rangeIndex); + LOG.debug("Didn't set segment id `{}` to slot {} because it was busy", segmentId, rangeIndex); } else { LOG.debug("Did set segment id `{}` to slot {}", segmentId, rangeIndex); - scheduleRetry = repairSegment(rangeIndex, nextRepairSegment.get().getId(), - nextRepairSegment.get().getTokenRange()); + scheduleRetry + = repairSegment(rangeIndex, nextRepairSegment.get().getId(), nextRepairSegment.get().getTokenRange()); if (!scheduleRetry) { break; } @@ -280,8 +290,7 @@ private void startNextSegment() throws ReaperException, InterruptedException { } if (!repairStarted && !anythingRunningStill) { - int amountDone = context.storage - .getSegmentAmountForRepairRunWithState(repairRunId, RepairSegment.State.DONE); + int amountDone = context.storage.getSegmentAmountForRepairRunWithState(repairRunId, RepairSegment.State.DONE); LOG.info("Repair amount done {}", amountDone); if (amountDone == context.storage.getSegmentAmountForRepairRun(repairRunId)) { endRepairRun(); @@ -297,12 +306,13 @@ private void startNextSegment() throws ReaperException, InterruptedException { /** * Start the repair of a segment. * - * @param segmentId id of the segment to repair. + * @param segmentId id of the segment to repair. * @param tokenRange token range of the segment to repair. * @return Boolean indicating whether rescheduling next run is needed. - * @throws InterruptedException */ - private boolean repairSegment(final int rangeIndex, final UUID segmentId, RingRange tokenRange) throws InterruptedException { + private boolean repairSegment(final int rangeIndex, final UUID segmentId, RingRange tokenRange) + throws InterruptedException { + final UUID unitId; final double intensity; final RepairParallelism validationParallelism; @@ -318,7 +328,7 @@ private boolean repairSegment(final int rangeIndex, final UUID segmentId, RingRa LOG.debug("preparing to repair segment {} on run with id {}", segmentId, repairRunId); try { - confirmJMXConnectionIsOpen(); + confirmJmxConnectionIsOpen(); } catch (ReaperException e) { LOG.warn("Failed to reestablish JMX connection in runner {}, retrying", repairRunId, e); currentlyRunningSegments.set(rangeIndex, null); @@ -326,73 +336,84 @@ private boolean repairSegment(final int rangeIndex, final UUID segmentId, RingRa } List potentialCoordinators; - if(!repairUnit.getIncrementalRepair()) { - // full repair - try { - potentialCoordinators = filterPotentialCoordinatorsByDatacenters(repairUnit.getDatacenters(), - jmxConnection.tokenRangeToEndpoint(keyspace, tokenRange), jmxConnection); - } catch (RuntimeException e) { - LOG.warn("Couldn't get token ranges from coordinator: #{}", e); - return true; - } - if (potentialCoordinators.isEmpty()) { - LOG.warn("Segment #{} is faulty, no potential coordinators for range: {}", - segmentId, tokenRange.toString()); - // This segment has a faulty token range. Abort the entire repair run. - synchronized (this) { - RepairRun repairRun = context.storage.getRepairRun(repairRunId).get(); - context.storage.updateRepairRun(repairRun - .with() - .runState(RepairRun.RunState.ERROR) - .lastEvent(String.format("No coordinators for range %s", tokenRange)) - .endTime(DateTime.now()) - .build(repairRunId)); - killAndCleanupRunner(); - } - return false; - } + if (!repairUnit.getIncrementalRepair()) { + // full repair + try { + potentialCoordinators = filterPotentialCoordinatorsByDatacenters( + repairUnit.getDatacenters(), jmxConnection.tokenRangeToEndpoint(keyspace, tokenRange), jmxConnection); + } catch (RuntimeException e) { + LOG.warn("Couldn't get token ranges from coordinator: #{}", e); + return true; + } + if (potentialCoordinators.isEmpty()) { + LOG.warn("Segment #{} is faulty, no potential coordinators for range: {}", segmentId, tokenRange.toString()); + // This segment has a faulty token range. Abort the entire repair run. + synchronized (this) { + RepairRun repairRun = context.storage.getRepairRun(repairRunId).get(); + context.storage.updateRepairRun( + repairRun + .with() + .runState(RepairRun.RunState.ERROR) + .lastEvent(String.format("No coordinators for range %s", tokenRange)) + .endTime(DateTime.now()) + .build(repairRunId)); + killAndCleanupRunner(); + } + return false; + } } else { - // Add random sleep time to avoid one Reaper instance locking all others during multi DC incremental repairs - Thread.sleep(ThreadLocalRandom.current().nextInt(0, 10 + 1)*1000); - potentialCoordinators = Arrays.asList(context.storage.getRepairSegment(repairRunId, segmentId).get().getCoordinatorHost()); + // Add random sleep time to avoid one Reaper instance locking all others during multi DC incremental repairs + Thread.sleep(ThreadLocalRandom.current().nextInt(0, 10 + 1) * 1000); + potentialCoordinators + = Arrays.asList(context.storage.getRepairSegment(repairRunId, segmentId).get().getCoordinatorHost()); } try { - SegmentRunner segmentRunner = new SegmentRunner(context, segmentId, potentialCoordinators, - context.repairManager.getRepairTimeoutMillis(), intensity, validationParallelism, - clusterName, repairUnit, this); - - ListenableFuture segmentResult = context.repairManager.submitSegment(segmentRunner); - Futures.addCallback(segmentResult, new FutureCallback() { - @Override - public void onSuccess(Object ignored) { - currentlyRunningSegments.set(rangeIndex, null); - handleResult(segmentId); - } - - @Override - public void onFailure(Throwable t) { - currentlyRunningSegments.set(rangeIndex, null); - LOG.error("Executing SegmentRunner failed", t); - } - }); + SegmentRunner segmentRunner = new SegmentRunner( + context, + segmentId, + potentialCoordinators, + context.repairManager.getRepairTimeoutMillis(), + intensity, + validationParallelism, + clusterName, + repairUnit, + this); + + ListenableFuture segmentResult = context.repairManager.submitSegment(segmentRunner); + Futures.addCallback( + segmentResult, + new FutureCallback() { + @Override + public void onSuccess(Object ignored) { + currentlyRunningSegments.set(rangeIndex, null); + handleResult(segmentId); + } + + @Override + public void onFailure(Throwable throwable) { + currentlyRunningSegments.set(rangeIndex, null); + LOG.error("Executing SegmentRunner failed", throwable); + } + }); } catch (ReaperException ex) { - LOG.error("Executing SegmentRunner failed", ex); + LOG.error("Executing SegmentRunner failed", ex); } return true; } - private List filterPotentialCoordinatorsByDatacenters(Collection datacenters, - List potentialCoordinators, JmxProxy jmxProxy) { + private List filterPotentialCoordinatorsByDatacenters( + Collection datacenters, + List potentialCoordinators, + JmxProxy jmxProxy) { - List coordinators = - potentialCoordinators - .stream() - .map(coord -> getNodeDatacenterPair(coord, jmxProxy)) - .filter(node -> datacenters.contains(node.getRight()) || datacenters.isEmpty()) - .map(nodeTuple -> nodeTuple.getLeft()) - .collect(Collectors.toList()); + List coordinators = potentialCoordinators + .stream() + .map(coord -> getNodeDatacenterPair(coord, jmxProxy)) + .filter(node -> datacenters.contains(node.getRight()) || datacenters.isEmpty()) + .map(nodeTuple -> nodeTuple.getLeft()) + .collect(Collectors.toList()); LOG.debug( "[filterPotentialCoordinatorsByDatacenters] coordinators filtered by dc {}. Before : {} / After : {}", @@ -414,27 +435,29 @@ private void handleResult(UUID segmentId) { // Don't do rescheduling here, not to spawn uncontrolled amount of threads if (segment.isPresent()) { - RepairSegment.State state = segment.get().getState(); - LOG.debug("In repair run #{}, triggerRepair on segment {} ended with state {}", repairRunId, segmentId, state); - switch (state) { - case NOT_STARTED: - // Unsuccessful repair - break; + RepairSegment.State state = segment.get().getState(); + LOG.debug("In repair run #{}, triggerRepair on segment {} ended with state {}", repairRunId, segmentId, state); + switch (state) { + case NOT_STARTED: + // Unsuccessful repair + break; - case DONE: - // Successful repair - break; + case DONE: + // Successful repair + break; - default: - // Another thread has started a new repair on this segment already - // Or maybe the same repair segment id should never be re-run in which case this is an error - String msg = "handleResult called with a segment state (" + state + ") that it " - + "should not have after segmentRunner has tried a repair"; - LOG.error(msg); - throw new AssertionError(msg); - } + default: + // Another thread has started a new repair on this segment already + // Or maybe the same repair segment id should never be re-run in which case this is an error + String msg = "handleResult called with a segment state (" + + state + + ") that it " + + "should not have after segmentRunner has tried a repair"; + LOG.error(msg); + throw new AssertionError(msg); + } } else { - LOG.warn("In repair run #{}, triggerRepair on segment {} ended, but run is missing", repairRunId, segmentId); + LOG.warn("In repair run #{}, triggerRepair on segment {} ended, but run is missing", repairRunId, segmentId); } } @@ -442,12 +465,10 @@ public void updateLastEvent(String newEvent) { synchronized (this) { RepairRun repairRun = context.storage.getRepairRun(repairRunId).get(); if (repairRun.getRunState().isTerminated()) { - LOG.warn("Will not update lastEvent of run that has already terminated. The message was: " - + "\"{}\"", newEvent); + LOG.warn( + "Will not update lastEvent of run that has already terminated. The message was: " + "\"{}\"", newEvent); } else { - context.storage.updateRepairRun(repairRun.with() - .lastEvent(newEvent) - .build(repairRunId)); + context.storage.updateRepairRun(repairRun.with().lastEvent(newEvent).build(repairRunId)); } } } @@ -463,6 +484,5 @@ public void killAndCleanupRunner() { } } Thread.currentThread().interrupt(); - return; } } diff --git a/src/server/src/main/java/com/spotify/reaper/service/RingRange.java b/src/server/src/main/java/com/spotify/reaper/service/RingRange.java index 03504215c..a4573e8d2 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/RingRange.java +++ b/src/server/src/main/java/com/spotify/reaper/service/RingRange.java @@ -11,17 +11,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.service; -import com.google.common.annotations.VisibleForTesting; import java.math.BigInteger; import java.util.Collections; import java.util.Comparator; import java.util.List; +import com.google.common.annotations.VisibleForTesting; + // TODO: Check if this duplicates org.apache.cassandra.dht.Range. -public class RingRange { +public final class RingRange { + + public static final Comparator START_COMPARATOR + = (RingRange o1, RingRange o2) -> o1.start.compareTo(o2.start); private final BigInteger start; private final BigInteger end; @@ -62,20 +67,15 @@ public BigInteger span(BigInteger ringSize) { */ public boolean encloses(RingRange other) { if (!isWrapping()) { - return !other.isWrapping() && - SegmentGenerator.greaterThanOrEqual(other.start, start) + return !other.isWrapping() + && SegmentGenerator.greaterThanOrEqual(other.start, start) && SegmentGenerator.lowerThanOrEqual(other.end, end); } else { - return - (!other.isWrapping() && - ( - SegmentGenerator.greaterThanOrEqual(other.start, start) || - SegmentGenerator.lowerThanOrEqual(other.end, end) - ) - ) || ( - SegmentGenerator.greaterThanOrEqual(other.start, start) && - SegmentGenerator.lowerThanOrEqual(other.end, end) - ); + return (!other.isWrapping() + && (SegmentGenerator.greaterThanOrEqual(other.start, start) + || SegmentGenerator.lowerThanOrEqual(other.end, end))) + || (SegmentGenerator.greaterThanOrEqual(other.start, start) + && SegmentGenerator.lowerThanOrEqual(other.end, end)); } } @@ -94,12 +94,12 @@ public String toString() { public static RingRange merge(List ranges) { - // sort - Collections.sort(ranges, startComparator); + // sor + Collections.sort(ranges, START_COMPARATOR); // find gap int gap = 0; - for (;gap ranges) { } // return merged - if (gap == ranges.size()-1) { + if (gap == ranges.size() - 1) { return new RingRange(ranges.get(0).start, ranges.get(gap).end); } else { - return new RingRange(ranges.get(gap+1).start, ranges.get(gap).end); + return new RingRange(ranges.get(gap + 1).start, ranges.get(gap).end); } } - - public static final Comparator startComparator = new Comparator() { - @Override - public int compare(RingRange o1, RingRange o2) { - return o1.start.compareTo(o2.start); - } - }; } diff --git a/src/server/src/main/java/com/spotify/reaper/service/SchedulingManager.java b/src/server/src/main/java/com/spotify/reaper/service/SchedulingManager.java index b1e442aea..fb8a808e7 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/SchedulingManager.java +++ b/src/server/src/main/java/com/spotify/reaper/service/SchedulingManager.java @@ -1,8 +1,19 @@ +/* + * 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.spotify.reaper.service; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.spotify.reaper.AppContext; import com.spotify.reaper.ReaperException; @@ -11,37 +22,49 @@ import com.spotify.reaper.core.RepairUnit; import com.spotify.reaper.resources.CommonTools; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Collection; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public final class SchedulingManager extends TimerTask { private static final Logger LOG = LoggerFactory.getLogger(SchedulingManager.class); - private static TimerTask schedulingManager; + private static volatile TimerTask SCHEDULING_MANAGER; + + private final AppContext context; + + /* nextActivatedSchedule used for nicer logging only */ + private RepairSchedule nextActivatedSchedule; + + + private SchedulingManager(AppContext context) { + this.context = context; + } public static void start(AppContext context) { - if (null == schedulingManager) { + if (null == SCHEDULING_MANAGER) { LOG.info("Starting new SchedulingManager instance"); - schedulingManager = new SchedulingManager(context); + SCHEDULING_MANAGER = new SchedulingManager(context); Timer timer = new Timer("SchedulingManagerTimer"); - timer.schedule(schedulingManager, 1000l, 1000l * 60); + timer.schedule(SCHEDULING_MANAGER, 1000L, 1000L * 60); } else { LOG.warn("there is already one instance of SchedulingManager running, not starting new one"); } } public static RepairSchedule pauseRepairSchedule(AppContext context, RepairSchedule schedule) { - RepairSchedule updatedSchedule = schedule.with() - .state(RepairSchedule.State.PAUSED) - .pauseTime(DateTime.now()) - .build(schedule.getId()); + RepairSchedule updatedSchedule + = schedule.with().state(RepairSchedule.State.PAUSED).pauseTime(DateTime.now()).build(schedule.getId()); + if (!context.storage.updateRepairSchedule(updatedSchedule)) { throw new IllegalStateException(String.format("failed updating repair schedule %s", updatedSchedule.getId())); } @@ -49,198 +72,190 @@ public static RepairSchedule pauseRepairSchedule(AppContext context, RepairSched } public static RepairSchedule resumeRepairSchedule(AppContext context, RepairSchedule schedule) { - RepairSchedule updatedSchedule = schedule.with() - .state(RepairSchedule.State.ACTIVE) - .pauseTime(null) - .build(schedule.getId()); + RepairSchedule updatedSchedule + = schedule.with().state(RepairSchedule.State.ACTIVE).pauseTime(null).build(schedule.getId()); + if (!context.storage.updateRepairSchedule(updatedSchedule)) { throw new IllegalStateException(String.format("failed updating repair schedule %s", updatedSchedule.getId())); } return updatedSchedule; } - private final AppContext context; - - /* nextActivatedSchedule used for nicer logging only */ - private RepairSchedule nextActivatedSchedule; - - private SchedulingManager(AppContext context) { - this.context = context; - } - /** * Called regularly, do not block! */ @Override public void run() { if (context.isRunning.get()) { - LOG.debug("Checking for repair schedules..."); - UUID lastId = null; + LOG.debug("Checking for repair schedules..."); + UUID lastId = null; + try { + Collection schedules = context.storage.getAllRepairSchedules(); + boolean anyRunStarted = false; + for (RepairSchedule schedule : schedules) { + lastId = schedule.getId(); + anyRunStarted = manageSchedule(schedule) || anyRunStarted; + } + if (!anyRunStarted && nextActivatedSchedule != null) { + LOG.debug( + "not scheduling new repairs yet, next activation is '{}' for schedule id '{}'", + nextActivatedSchedule.getNextActivation(), + nextActivatedSchedule.getId()); + } + } catch (Throwable ex) { + LOG.error("failed managing schedule for run with id: {}", lastId); + LOG.error("catch exception", ex); try { - Collection schedules = context.storage.getAllRepairSchedules(); - boolean anyRunStarted = false; - for (RepairSchedule schedule : schedules) { - lastId = schedule.getId(); - anyRunStarted = manageSchedule(schedule) || anyRunStarted; - } - if (!anyRunStarted && nextActivatedSchedule != null) { - LOG.debug("not scheduling new repairs yet, next activation is '{}' for schedule id '{}'", - nextActivatedSchedule.getNextActivation(), nextActivatedSchedule.getId()); - } - } catch (Exception ex) { - LOG.error("failed managing schedule for run with id: {}", lastId); - LOG.error("catch exception", ex); - try { - assert false : "if assertions are enabled then exit the jvm"; - } catch (AssertionError ae) { - if (context.isRunning.get()) { - LOG.error("SchedulingManager failed. Exiting JVM."); - System.exit(1); - } + assert false : "if assertions are enabled then exit the jvm"; + } catch (AssertionError ae) { + if (context.isRunning.get()) { + LOG.error("SchedulingManager failed. Exiting JVM."); + System.exit(1); } } + } } } /** * Manage, i.e. check whether a new repair run should be started with this schedule. + * * @param schedule The schedule to be checked for activation. * @return boolean indicating whether a new RepairRun instance was created and started. */ - private boolean manageSchedule(RepairSchedule schedule_) { - switch (schedule_.getState()) { - case ACTIVE: - if (schedule_.getNextActivation().isBeforeNow()) { + private boolean manageSchedule(RepairSchedule schdle) { + switch (schdle.getState()) { + case ACTIVE: + if (schdle.getNextActivation().isBeforeNow()) { - RepairSchedule schedule = schedule_ - .with().nextActivation(schedule_.getFollowingActivation()).build(schedule_.getId()); + RepairSchedule schedule + = schdle.with().nextActivation(schdle.getFollowingActivation()).build(schdle.getId()); - context.storage.updateRepairSchedule(schedule); + context.storage.updateRepairSchedule(schedule); - LOG.info( - "repair unit '{}' should be repaired based on RepairSchedule with id '{}'", - schedule.getRepairUnitId(), schedule.getId()); + LOG.info( + "repair unit '{}' should be repaired based on RepairSchedule with id '{}'", + schedule.getRepairUnitId(), + schedule.getId()); - Optional fetchedUnit = context.storage.getRepairUnit(schedule.getRepairUnitId()); - if (!fetchedUnit.isPresent()) { - LOG.warn("RepairUnit with id {} not found", schedule.getRepairUnitId()); - return false; - } - RepairUnit repairUnit = fetchedUnit.get(); - if (repairRunAlreadyScheduled(schedule, repairUnit)) { - return false; - } + Optional fetchedUnit = context.storage.getRepairUnit(schedule.getRepairUnitId()); + if (!fetchedUnit.isPresent()) { + LOG.warn("RepairUnit with id {} not found", schedule.getRepairUnitId()); + return false; + } + RepairUnit repairUnit = fetchedUnit.get(); + if (repairRunAlreadyScheduled(schedule, repairUnit)) { + return false; + } - try { - RepairRun newRepairRun = createNewRunForUnit(schedule, repairUnit); - - ImmutableList newRunHistory = new ImmutableList.Builder() - .addAll(schedule.getRunHistory()).add(newRepairRun.getId()).build(); - - RepairSchedule latestSchedule = context.storage.getRepairSchedule(schedule.getId()).get(); - - if (equal(schedule, latestSchedule)) { - - boolean result = context.storage.updateRepairSchedule( - schedule.with() - .runHistory(newRunHistory) - .build(schedule.getId())); - // FIXME – concurrency is broken unless we atomically add/remove run history items - //boolean result = context.storage - // .addRepairRunToRepairSchedule(schedule.getId(), newRepairRun.getId()); - - if (result) { - context.repairManager.startRepairRun(context, newRepairRun); - return true; - } - } else if (schedule.getRunHistory().size() < latestSchedule.getRunHistory().size()) { - UUID newRepairRunId = latestSchedule.getRunHistory().get(latestSchedule.getRunHistory().size()-1); - LOG.info("schedule {} has already added a new repair run {}", schedule.getId(), newRepairRunId); - // this repair_run is identified as a duplicate (for this activation): - // so take the last repair run, and try start it. it's ok if already running. - newRepairRun = context.storage.getRepairRun(newRepairRunId).get(); - context.repairManager.startRepairRun(context, newRepairRun); - } else { - LOG.warn("schedule {} has been altered by someone else. not running repair", schedule.getId()); - } - // this duplicated repair_run needs to be removed from the schedule's history - // FIXME – concurrency is broken unless we atomically add/remove run history items - //boolean result = context.storage - // .deleteRepairRunFromRepairSchedule(schedule.getId(), newRepairRun.getId()); - } catch (ReaperException e) { - LOG.error(e.getMessage(), e); - } - } else { - if (nextActivatedSchedule == null - || nextActivatedSchedule.getNextActivation().isAfter(schedule_.getNextActivation())) { + try { + RepairRun newRepairRun = createNewRunForUnit(schedule, repairUnit); + + ImmutableList newRunHistory + = new ImmutableList.Builder().addAll(schedule.getRunHistory()).add(newRepairRun.getId()).build(); + + RepairSchedule latestSchedule = context.storage.getRepairSchedule(schedule.getId()).get(); + + if (equal(schedule, latestSchedule)) { + + boolean result = context.storage.updateRepairSchedule( + schedule.with().runHistory(newRunHistory).build(schedule.getId())); + // FIXME – concurrency is broken unless we atomically add/remove run history items + // boolean result = context.storage + // .addRepairRunToRepairSchedule(schedule.getId(), newRepairRun.getId()); - nextActivatedSchedule = schedule_; + if (result) { + context.repairManager.startRepairRun(context, newRepairRun); + return true; } + } else if (schedule.getRunHistory().size() < latestSchedule.getRunHistory().size()) { + UUID newRepairRunId = latestSchedule.getRunHistory().get(latestSchedule.getRunHistory().size() - 1); + LOG.info("schedule {} has already added a new repair run {}", schedule.getId(), newRepairRunId); + // this repair_run is identified as a duplicate (for this activation): + // so take the last repair run, and try start it. it's ok if already running. + newRepairRun = context.storage.getRepairRun(newRepairRunId).get(); + context.repairManager.startRepairRun(context, newRepairRun); + } else { + LOG.warn("schedule {} has been altered by someone else. not running repair", schedule.getId()); } - break; - case PAUSED: - LOG.info("Repair schedule '{}' is paused", schedule_.getId()); - return false; - default: - throw new AssertionError("illegal schedule state in call to manageSchedule(..): " + schedule_.getState()); + // this duplicated repair_run needs to be removed from the schedule's history + // FIXME – concurrency is broken unless we atomically add/remove run history items + // boolean result = context.storage + // .deleteRepairRunFromRepairSchedule(schedule.getId(), newRepairRun.getId()); + } catch (ReaperException e) { + LOG.error(e.getMessage(), e); + } + } else { + if (nextActivatedSchedule == null + || nextActivatedSchedule.getNextActivation().isAfter(schdle.getNextActivation())) { + + nextActivatedSchedule = schdle; + } + } + break; + case PAUSED: + LOG.info("Repair schedule '{}' is paused", schdle.getId()); + return false; + default: + throw new AssertionError("illegal schedule state in call to manageSchedule(..): " + schdle.getState()); } return false; } - private static boolean equal(RepairSchedule s1, RepairSchedule s2) { - Preconditions.checkArgument(s1.getId().equals(s2.getId())); - Preconditions.checkArgument(s1.getOwner().equals(s2.getOwner())); - Preconditions.checkArgument(s1.getDaysBetween() == s2.getDaysBetween()); - Preconditions.checkArgument(s1.getIntensity() == s2.getIntensity()); - Preconditions.checkArgument(s1.getCreationTime().equals(s2.getCreationTime())); - Preconditions.checkArgument(s1.getNextActivation().equals(s2.getNextActivation())); - Preconditions.checkArgument(s1.getFollowingActivation().equals(s2.getFollowingActivation())); + private static boolean equal(RepairSchedule s1, RepairSchedule s2) { + Preconditions.checkArgument(s1.getId().equals(s2.getId())); + Preconditions.checkArgument(s1.getOwner().equals(s2.getOwner())); + Preconditions.checkArgument(s1.getDaysBetween() == s2.getDaysBetween()); + Preconditions.checkArgument(s1.getIntensity() == s2.getIntensity()); + Preconditions.checkArgument(s1.getCreationTime().equals(s2.getCreationTime())); + Preconditions.checkArgument(s1.getNextActivation().equals(s2.getNextActivation())); + Preconditions.checkArgument(s1.getFollowingActivation().equals(s2.getFollowingActivation())); - boolean result = s1.getState().equals(s2.getState()); - result &= s1.getRunHistory().size() == s2.getRunHistory().size(); + boolean result = s1.getState().equals(s2.getState()); + result &= s1.getRunHistory().size() == s2.getRunHistory().size(); - for (int i = 0 ; result && i < s1.getRunHistory().size() ; ++i) { - result &= s1.getRunHistory().get(i).equals(s2.getRunHistory().get(i)); - } - return result; + for (int i = 0; result && i < s1.getRunHistory().size(); ++i) { + result &= s1.getRunHistory().get(i).equals(s2.getRunHistory().get(i)); } + return result; + } - private boolean repairRunAlreadyScheduled(RepairSchedule schedule, RepairUnit repairUnit) { - Collection repairRuns = context.storage.getRepairRunsForUnit(schedule.getRepairUnitId()); - for (RepairRun repairRun : repairRuns) { - if (repairRunComesFromSchedule(repairRun, schedule)) { - LOG.info( - "there is repair (id {}) in state '{}' for repair unit '{}', " - + "postponing current schedule trigger until next scheduling", - repairRun.getId(), repairRun.getRunState(), repairUnit.getId()); - return true; - } - } - return false; + private boolean repairRunAlreadyScheduled(RepairSchedule schedule, RepairUnit repairUnit) { + Collection repairRuns = context.storage.getRepairRunsForUnit(schedule.getRepairUnitId()); + for (RepairRun repairRun : repairRuns) { + if (repairRunComesFromSchedule(repairRun, schedule)) { + LOG.info( + "there is repair (id {}) in state '{}' for repair unit '{}', " + + "postponing current schedule trigger until next scheduling", + repairRun.getId(), + repairRun.getRunState(), + repairUnit.getId()); + return true; + } } + return false; + } private static boolean repairRunComesFromSchedule(RepairRun repairRun, RepairSchedule schedule) { return repairRun.getRunState().isActive() - || (RepairRun.RunState.NOT_STARTED == repairRun.getRunState() - && repairRun.getCause().equals(getCauseName(schedule))); + || (RepairRun.RunState.NOT_STARTED == repairRun.getRunState() + && repairRun.getCause().equals(getCauseName(schedule))); } - private RepairRun createNewRunForUnit(RepairSchedule schedule, RepairUnit repairUnit) throws ReaperException { return CommonTools.registerRepairRun( - context, - context.storage.getCluster(repairUnit.getClusterName()).get(), - repairUnit, - Optional.of(getCauseName(schedule)), - schedule.getOwner(), - schedule.getSegmentCount(), - schedule.getRepairParallelism(), - schedule.getIntensity()); + context, + context.storage.getCluster(repairUnit.getClusterName()).get(), + repairUnit, + Optional.of(getCauseName(schedule)), + schedule.getOwner(), + schedule.getSegmentCount(), + schedule.getRepairParallelism(), + schedule.getIntensity()); } private static String getCauseName(RepairSchedule schedule) { - return "scheduled run (schedule id " + schedule.getId().toString() + ')'; + return "scheduled run (schedule id " + schedule.getId().toString() + ')'; } - } diff --git a/src/server/src/main/java/com/spotify/reaper/service/SegmentGenerator.java b/src/server/src/main/java/com/spotify/reaper/service/SegmentGenerator.java index e981d4b41..eeb45fe0e 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/SegmentGenerator.java +++ b/src/server/src/main/java/com/spotify/reaper/service/SegmentGenerator.java @@ -11,94 +11,92 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper.service; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; +package com.spotify.reaper.service; import com.spotify.reaper.ReaperException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.math.BigInteger; import java.util.List; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Splits given Cassandra table's token range into RepairSegments. */ -public class SegmentGenerator { +public final class SegmentGenerator { private static final Logger LOG = LoggerFactory.getLogger(SegmentGenerator.class); private final String partitioner; - private final BigInteger RANGE_MIN; - private final BigInteger RANGE_MAX; - private final BigInteger RANGE_SIZE; + private final BigInteger rangeMin; + private final BigInteger rangeMax; + private final BigInteger rangeSize; public SegmentGenerator(String partitioner) throws ReaperException { if (partitioner.endsWith("RandomPartitioner")) { - RANGE_MIN = BigInteger.ZERO; - RANGE_MAX = new BigInteger("2").pow(127).subtract(BigInteger.ONE); + rangeMin = BigInteger.ZERO; + rangeMax = new BigInteger("2").pow(127).subtract(BigInteger.ONE); } else if (partitioner.endsWith("Murmur3Partitioner")) { - RANGE_MIN = new BigInteger("2").pow(63).negate(); - RANGE_MAX = new BigInteger("2").pow(63).subtract(BigInteger.ONE); + rangeMin = new BigInteger("2").pow(63).negate(); + rangeMax = new BigInteger("2").pow(63).subtract(BigInteger.ONE); } else { throw new ReaperException("Unsupported partitioner " + partitioner); } - RANGE_SIZE = RANGE_MAX.subtract(RANGE_MIN).add(BigInteger.ONE); + rangeSize = rangeMax.subtract(rangeMin).add(BigInteger.ONE); this.partitioner = partitioner; } public SegmentGenerator(BigInteger rangeMin, BigInteger rangeMax) { - RANGE_MIN = rangeMin; - RANGE_MAX = rangeMax; - RANGE_SIZE = RANGE_MAX.subtract(RANGE_MIN).add(BigInteger.ONE); + this.rangeMin = rangeMin; + this.rangeMax = rangeMax; + rangeSize = rangeMax.subtract(rangeMin).add(BigInteger.ONE); partitioner = "(" + rangeMin + "," + rangeMax + ")"; } @VisibleForTesting - public static BigInteger max(BigInteger a, BigInteger b) { - return greaterThan(a, b) ? a : b; + public static BigInteger max(BigInteger big0, BigInteger big1) { + return greaterThan(big0, big1) ? big0 : big1; } @VisibleForTesting - public static BigInteger min(BigInteger a, BigInteger b) { - return lowerThan(a, b) ? a : b; + public static BigInteger min(BigInteger big0, BigInteger big1) { + return lowerThan(big0, big1) ? big0 : big1; } @VisibleForTesting - public static boolean lowerThan(BigInteger a, BigInteger b) { - return a.compareTo(b) < 0; + public static boolean lowerThan(BigInteger big0, BigInteger big1) { + return big0.compareTo(big1) < 0; } @VisibleForTesting - public static boolean lowerThanOrEqual(BigInteger a, BigInteger b) { - return a.compareTo(b) <= 0; + public static boolean lowerThanOrEqual(BigInteger big0, BigInteger big1) { + return big0.compareTo(big1) <= 0; } @VisibleForTesting - public static boolean greaterThan(BigInteger a, BigInteger b) { - return a.compareTo(b) > 0; + public static boolean greaterThan(BigInteger big0, BigInteger big1) { + return big0.compareTo(big1) > 0; } @VisibleForTesting - public static boolean greaterThanOrEqual(BigInteger a, BigInteger b) { - return a.compareTo(b) >= 0; + public static boolean greaterThanOrEqual(BigInteger big0, BigInteger big1) { + return big0.compareTo(big1) >= 0; } /** - * Given a properly ordered list of tokens, compute at least {@code totalSegmentCount} repair - * segments. + * Given big0 properly ordered list of tokens, compute at least {@code totalSegmentCount} repair segments. * - * @param totalSegmentCount requested total amount of repair segments. This function may generate - * more segments. - * @param ringTokens list of all start tokens in a cluster. They have to be in ring order. - * @param incrementalRepair - * @return a list containing at least {@code totalSegmentCount} repair segments. + * @param totalSegmentCount requested total amount of repair segments. This function may generate more segments. + * @param ringTokens list of all start tokens in big0 cluster. They have to be in ring order. + * @return big0 list containing at least {@code totalSegmentCount} repair segments. */ public List generateSegments(int totalSegmentCount, List ringTokens, Boolean incrementalRepair) throws ReaperException { + int tokenRangeCount = ringTokens.size(); List repairSegments = Lists.newArrayList(); @@ -107,38 +105,35 @@ public List generateSegments(int totalSegmentCount, List BigInteger stop = ringTokens.get((i + 1) % tokenRangeCount); if (!inRange(start) || !inRange(stop)) { - throw new ReaperException(String.format("Tokens (%s,%s) not in range of %s", - start, stop, partitioner)); + throw new ReaperException(String.format("Tokens (%s,%s) not in range of %s", start, stop, partitioner)); } if (start.equals(stop) && tokenRangeCount != 1) { - throw new ReaperException(String.format("Tokens (%s,%s): two nodes have the same token", - start, stop)); + throw new ReaperException(String.format("Tokens (%s,%s): two nodes have the same token", start, stop)); } - BigInteger rangeSize = stop.subtract(start); - if (lowerThanOrEqual(rangeSize, BigInteger.ZERO)) { + BigInteger rs = stop.subtract(start); + if (lowerThanOrEqual(rs, BigInteger.ZERO)) { // wrap around case - rangeSize = rangeSize.add(RANGE_SIZE); + rs = rs.add(rangeSize); } // the below, in essence, does this: // segmentCount = ceiling((rangeSize / RANGE_SIZE) * totalSegmentCount) - BigInteger[] segmentCountAndRemainder = - rangeSize.multiply(BigInteger.valueOf(totalSegmentCount)).divideAndRemainder(RANGE_SIZE); - int segmentCount = segmentCountAndRemainder[0].intValue() + - (segmentCountAndRemainder[1].equals(BigInteger.ZERO) ? 0 : 1); + BigInteger[] segmentCountAndRemainder + = rs.multiply(BigInteger.valueOf(totalSegmentCount)).divideAndRemainder(rangeSize); + + int segmentCount = segmentCountAndRemainder[0].intValue() + + (segmentCountAndRemainder[1].equals(BigInteger.ZERO) ? 0 : 1); LOG.info("Dividing token range [{},{}) into {} segments", start, stop, segmentCount); - // Make a list of all the endpoints for the repair segments, including both start and stop + // Make big0 list of all the endpoints for the repair segments, including both start and stop List endpointTokens = Lists.newArrayList(); for (int j = 0; j <= segmentCount; j++) { - BigInteger offset = rangeSize - .multiply(BigInteger.valueOf(j)) - .divide(BigInteger.valueOf(segmentCount)); + BigInteger offset = rs.multiply(BigInteger.valueOf(j)).divide(BigInteger.valueOf(segmentCount)); BigInteger reaperToken = start.add(offset); - if (greaterThan(reaperToken, RANGE_MAX)) { - reaperToken = reaperToken.subtract(RANGE_SIZE); + if (greaterThan(reaperToken, rangeMax)) { + reaperToken = reaperToken.subtract(rangeSize); } endpointTokens.add(reaperToken); } @@ -146,25 +141,23 @@ public List generateSegments(int totalSegmentCount, List // Append the segments between the endpoints for (int j = 0; j < segmentCount; j++) { repairSegments.add(new RingRange(endpointTokens.get(j), endpointTokens.get(j + 1))); - LOG.debug("Segment #{}: [{},{})", j + 1, endpointTokens.get(j), - endpointTokens.get(j + 1)); + LOG.debug("Segment #{}: [{},{})", j + 1, endpointTokens.get(j), endpointTokens.get(j + 1)); } } // verify that the whole range is repaired BigInteger total = BigInteger.ZERO; for (RingRange segment : repairSegments) { - BigInteger size = segment.span(RANGE_SIZE); + BigInteger size = segment.span(rangeSize); total = total.add(size); } - if (!total.equals(RANGE_SIZE) && !incrementalRepair) { + if (!total.equals(rangeSize) && !incrementalRepair) { throw new ReaperException("Not entire ring would get repaired"); } return repairSegments; } protected boolean inRange(BigInteger token) { - return !(lowerThan(token, RANGE_MIN) || greaterThan(token, RANGE_MAX)); + return !(lowerThan(token, rangeMin) || greaterThan(token, rangeMax)); } - } diff --git a/src/server/src/main/java/com/spotify/reaper/service/SegmentRunner.java b/src/server/src/main/java/com/spotify/reaper/service/SegmentRunner.java index 041932947..00512e872 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/SegmentRunner.java +++ b/src/server/src/main/java/com/spotify/reaper/service/SegmentRunner.java @@ -11,8 +11,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.service; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperApplicationConfiguration.DatacenterAvailability; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxConnectionFactory; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.cassandra.RepairStatusHandler; +import com.spotify.reaper.core.NodeMetrics; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.storage.IDistributedStorage; + import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.util.Collection; @@ -31,7 +43,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.management.JMException; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.sun.management.UnixOperatingSystemMXBean; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.progress.ProgressEventType; @@ -43,33 +64,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplicationConfiguration.DatacenterAvailability; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxConnectionFactory; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.cassandra.RepairStatusHandler; -import com.spotify.reaper.core.NodeMetrics; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.storage.IDistributedStorage; -import com.sun.management.UnixOperatingSystemMXBean; public final class SegmentRunner implements RepairStatusHandler, Runnable { + // Caching all active SegmentRunners. + @VisibleForTesting + public static final Map SEGMENT_RUNNERS = Maps.newConcurrentMap(); + private static final Logger LOG = LoggerFactory.getLogger(SegmentRunner.class); private static final int MAX_PENDING_COMPACTIONS = 20; private static final int MAX_TIMEOUT_EXTENSIONS = 10; private static final Pattern REPAIR_UUID_PATTERN - = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); private static final long SLEEP_TIME_AFTER_POSTPONE_IN_MS = 10000; private static final ExecutorService METRICS_GRABBER_EXECUTOR = Executors.newFixedThreadPool(10); @@ -88,15 +95,20 @@ public final class SegmentRunner implements RepairStatusHandler, Runnable { private final AtomicBoolean segmentFailed; private final UUID leaderElectionId; - // Caching all active SegmentRunners. - @VisibleForTesting - public static Map segmentRunners = Maps.newConcurrentMap(); - public SegmentRunner(AppContext context, UUID segmentId, Collection potentialCoordinators, - long timeoutMillis, double intensity, RepairParallelism validationParallelism, - String clusterName, RepairUnit repairUnit, RepairRunner repairRunner) throws ReaperException { + public SegmentRunner( + AppContext context, + UUID segmentId, + Collection potentialCoordinators, + long timeoutMillis, + double intensity, + RepairParallelism validationParallelism, + String clusterName, + RepairUnit repairUnit, + RepairRunner repairRunner) + throws ReaperException { - if (segmentRunners.containsKey(segmentId)) { + if (SEGMENT_RUNNERS.containsKey(segmentId)) { LOG.error("SegmentRunner already exists for segment with ID: {}", segmentId); throw new ReaperException("SegmentRunner already exists for segment with ID: " + segmentId); } @@ -118,34 +130,39 @@ public void run() { final RepairSegment segment = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); Thread.currentThread().setName(clusterName + ":" + segment.getRunId() + ":" + segmentId); if (takeLead()) { - try { - if(runRepair()) { - long delay = intensityBasedDelayMillis(intensity); - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - LOG.warn("Slept shorter than intended delay."); - } + try { + if (runRepair()) { + long delay = intensityBasedDelayMillis(intensity); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + LOG.warn("Slept shorter than intended delay."); } - } finally { - releaseLead(); } + } finally { + releaseLead(); + } } } public static void postpone(AppContext context, RepairSegment segment, Optional repairUnit) { LOG.info("Postponing segment {}", segment.getId()); try { - context.storage.updateRepairSegment(segment.with() - .state(RepairSegment.State.NOT_STARTED) - .coordinatorHost(repairUnit.isPresent() && repairUnit.get().getIncrementalRepair()? segment.getCoordinatorHost():null) // set coordinator host to null only for full repairs - .repairCommandId(null) - .startTime(null) - .failCount(segment.getFailCount() + 1) - .build(segment.getId())); + context.storage.updateRepairSegment( + segment + .with() + .state(RepairSegment.State.NOT_STARTED) + .coordinatorHost( + repairUnit.isPresent() && repairUnit.get().getIncrementalRepair() + ? segment.getCoordinatorHost() + : null) // set coordinator host to null only for full repairs + .repairCommandId(null) + .startTime(null) + .failCount(segment.getFailCount() + 1) + .build(segment.getId())); } finally { - segmentRunners.remove(segment.getId()); - context.metricRegistry.counter(metricNameForPostpone(repairUnit, segment)).inc(); + SEGMENT_RUNNERS.remove(segment.getId()); + context.metricRegistry.counter(metricNameForPostpone(repairUnit, segment)).inc(); } } @@ -157,6 +174,10 @@ public static void abort(AppContext context, RepairSegment segment, JmxProxy jmx jmxConnection.cancelAllRepairs(); } + private void abort(RepairSegment segment, JmxProxy jmxConnection) { + abort(context, segment, jmxConnection); + } + /** * Remember to call method postponeCurrentSegment() outside of synchronized(condition) block. */ @@ -174,8 +195,7 @@ public void postponeCurrentSegment() { } /** - * This method is intended to be temporary, until we find the root issue of too many open files - * issue. + * This method is intended to be temporary, until we find the root issue of too many open files issue. */ private static long getOpenFilesAmount() { OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); @@ -191,18 +211,17 @@ private boolean runRepair() { final RepairSegment segment = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); try (Timer.Context cxt = context.metricRegistry.timer(metricNameForRunRepair(segment)).time(); - JmxProxy coordinator = context.jmxConnectionFactory - .connectAny( - Optional.fromNullable(this), - potentialCoordinators, - context.config.getJmxConnectionTimeoutInSeconds())) { + JmxProxy coordinator = context.jmxConnectionFactory.connectAny( + Optional.fromNullable(this), + potentialCoordinators, + context.config.getJmxConnectionTimeoutInSeconds())) { - if (segmentRunners.containsKey(segmentId)) { + if (SEGMENT_RUNNERS.containsKey(segmentId)) { LOG.error("SegmentRunner already exists for segment with ID: {}", segmentId); closeJmxConnection(Optional.fromNullable(coordinator)); throw new ReaperException("SegmentRunner already exists for segment with ID: " + segmentId); } - segmentRunners.put(segmentId, this); + SEGMENT_RUNNERS.put(segmentId, this); String keyspace = repairUnit.getKeyspaceName(); boolean fullRepair = !repairUnit.getIncrementalRepair(); @@ -215,7 +234,8 @@ protected Set initialize() { Collection ongoingRepairs = context.storage.getOngoingRepairsInCluster(clusterName); Set busyHosts = Sets.newHashSet(); for (RepairParameters ongoingRepair : ongoingRepairs) { - busyHosts.addAll(coordinator.tokenRangeToEndpoint(ongoingRepair.keyspaceName, ongoingRepair.tokenRange)); + busyHosts.addAll( + coordinator.tokenRangeToEndpoint(ongoingRepair.keyspaceName, ongoingRepair.tokenRange)); } return busyHosts; } @@ -229,45 +249,54 @@ protected Set initialize() { try (Timer.Context cxt1 = context.metricRegistry.timer(metricNameForRepairing(segment)).time()) { LOG.debug("Enter synchronized section with segment ID {}", segmentId); synchronized (condition) { - commandId = coordinator.triggerRepair(segment.getStartToken(), segment.getEndToken(), - keyspace, validationParallelism, repairUnit.getColumnFamilies(), fullRepair, repairUnit.getDatacenters()); + commandId = coordinator.triggerRepair( + segment.getStartToken(), + segment.getEndToken(), + keyspace, + validationParallelism, + repairUnit.getColumnFamilies(), + fullRepair, + repairUnit.getDatacenters()); if (commandId == 0) { // From cassandra source in "forceRepairAsync": - //if (ranges.isEmpty() || Keyspace.open(keyspace).getReplicationStrategy().getReplicationFactor() < 2) + // if (ranges.isEmpty() || Keyspace.open(keyspace).getReplicationStrategy().getReplicationFactor() < 2) // return 0; LOG.info("Nothing to repair for keyspace {}", keyspace); - context.storage.updateRepairSegment(segment.with() - .coordinatorHost(coordinator.getHost()) - .state(RepairSegment.State.DONE) - .build(segmentId)); - segmentRunners.remove(segment.getId()); + context.storage.updateRepairSegment( + segment.with().coordinatorHost(coordinator.getHost()).state(RepairSegment.State.DONE).build(segmentId)); + + SEGMENT_RUNNERS.remove(segment.getId()); closeJmxConnection(Optional.fromNullable(coordinator)); return true; } LOG.debug("Triggered repair with command id {}", commandId); - // incremental repair can take way more time for a segment so we're extending the timeout MAX_TIMEOUT_EXTENSIONS times - long timeout = repairUnit.getIncrementalRepair()?timeoutMillis*MAX_TIMEOUT_EXTENSIONS:timeoutMillis; - context.storage.updateRepairSegment(segment.with() - .coordinatorHost(coordinator.getHost()) - .repairCommandId(commandId) - .build(segmentId)); - String eventMsg = String.format("Triggered repair of segment %s via host %s", segment.getId(), coordinator.getHost()); + // incremental repair can take way more time for a segment so we're extending the timeou + // MAX_TIMEOUT_EXTENSIONS times + long timeout = repairUnit.getIncrementalRepair() ? timeoutMillis * MAX_TIMEOUT_EXTENSIONS : timeoutMillis; + context.storage.updateRepairSegment( + segment.with().coordinatorHost(coordinator.getHost()).repairCommandId(commandId).build(segmentId)); + + String eventMsg + = String.format("Triggered repair of segment %s via host %s", segment.getId(), coordinator.getHost()); + repairRunner.updateLastEvent(eventMsg); LOG.info("Repair for segment {} started, status wait will timeout in {} millis", segmentId, timeout); try { long startTime = System.currentTimeMillis(); long maxTime = startTime + timeoutMillis; - // If timeout is lower than 1mn, use timeout, otherwise we'll loop every minute to renew lead on segment - long waitTime = timeoutMillis<60000?timeoutMillis:60000; + // If timeout is lower than 1mn, use timeout, otherwise we'll loop every minute to renew lead on segmen + long waitTime = timeoutMillis < 60000 ? timeoutMillis : 60000; long lastLoopTime = System.currentTimeMillis(); while (System.currentTimeMillis() < maxTime) { condition.await(waitTime, TimeUnit.MILLISECONDS); - if(lastLoopTime + 60_000 > System.currentTimeMillis() || context.storage.getRepairSegment(segment.getRunId(), segmentId).get().getState() == RepairSegment.State.DONE){ + if (lastLoopTime + 60_000 > System.currentTimeMillis() + || context.storage.getRepairSegment(segment.getRunId(), segmentId).get().getState() + == RepairSegment.State.DONE) { // The condition has been interrupted, meaning the repair might be over break; } @@ -279,16 +308,24 @@ protected Set initialize() { } catch (InterruptedException e) { LOG.warn("Repair command {} on segment {} interrupted", commandId, segmentId, e); } finally { - RepairSegment resultingSegment = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); - LOG.info("Repair command {} on segment {} returned with state {}", commandId, segmentId, resultingSegment.getState()); + RepairSegment resultingSegment + = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); + LOG.info( + "Repair command {} on segment {} returned with state {}", + commandId, + segmentId, + resultingSegment.getState()); if (resultingSegment.getState() == RepairSegment.State.RUNNING) { LOG.info("Repair command {} on segment {} has been cancelled while running", commandId, segmentId); segmentFailed.set(true); abort(resultingSegment, coordinator); } else if (resultingSegment.getState() == RepairSegment.State.DONE) { - LOG.debug("Repair segment with id '{}' was repaired in {} seconds", resultingSegment.getId(), + LOG.debug( + "Repair segment with id '{}' was repaired in {} seconds", + resultingSegment.getId(), Seconds.secondsBetween(resultingSegment.getStartTime(), resultingSegment.getEndTime()).getSeconds()); - segmentRunners.remove(resultingSegment.getId()); + + SEGMENT_RUNNERS.remove(resultingSegment.getId()); } // Repair is still running, we'll renew lead on the segment when using Cassandra as storage backend renewLead(); @@ -304,9 +341,9 @@ protected Set initialize() { LOG.warn("Open files amount for process: " + getOpenFilesAmount()); return false; } finally { - context.metricRegistry - .histogram(MetricRegistry.name(SegmentRunner.class, "open-files")) - .update(getOpenFilesAmount()); + context.metricRegistry + .histogram(MetricRegistry.name(SegmentRunner.class, "open-files")) + .update(getOpenFilesAmount()); } LOG.debug("Exiting synchronized section with segment ID {}", segmentId); return true; @@ -314,67 +351,76 @@ protected Set initialize() { private static String metricNameForPostpone(Optional unit, RepairSegment segment) { return unit.isPresent() - ? MetricRegistry.name( - SegmentRunner.class, - "postpone", - Optional.fromNullable(segment.getCoordinatorHost()).or("null").replace('.', '-'), - unit.get().getClusterName(), - unit.get().getKeyspaceName()) - : MetricRegistry.name( - SegmentRunner.class, - "postpone", - Optional.fromNullable(segment.getCoordinatorHost()).or("null") .replace('.', '-')); + ? MetricRegistry.name( + SegmentRunner.class, + "postpone", + Optional.fromNullable(segment.getCoordinatorHost()).or("null").replace('.', '-'), + unit.get().getClusterName(), + unit.get().getKeyspaceName()) + : MetricRegistry.name( + SegmentRunner.class, + "postpone", + Optional.fromNullable(segment.getCoordinatorHost()).or("null").replace('.', '-')); } private String metricNameForRepairing(RepairSegment rs) { return MetricRegistry.name( - SegmentRunner.class, - "repairing", - Optional.fromNullable(rs.getCoordinatorHost()).or("null").replace('.', '-'), - clusterName, - repairUnit.getKeyspaceName()); + SegmentRunner.class, + "repairing", + Optional.fromNullable(rs.getCoordinatorHost()).or("null").replace('.', '-'), + clusterName, + repairUnit.getKeyspaceName()); } private String metricNameForRunRepair(RepairSegment rs) { return MetricRegistry.name( - SegmentRunner.class, - "runRepair", - Optional.fromNullable(rs.getCoordinatorHost()).or("null").replace('.', '-'), - clusterName, - repairUnit.getKeyspaceName()); + SegmentRunner.class, + "runRepair", + Optional.fromNullable(rs.getCoordinatorHost()).or("null").replace('.', '-'), + clusterName, + repairUnit.getKeyspaceName()); } private void closeJmxConnection(Optional jmxProxy) { - if(jmxProxy.isPresent()) + if (jmxProxy.isPresent()) { try { jmxProxy.get().close(); } catch (ReaperException e) { LOG.warn("Could not close JMX connection to {}. Potential leak...", jmxProxy.get().getHost()); } + } } private void declineRun() { - LOG.info("SegmentRunner declined to repair segment {} because only one segment is allowed at once for incremental repairs", - segmentId); + LOG.info( + "SegmentRunner declined to repair segment {} " + + "because only one segment is allowed at once for incremental repairs", + segmentId); + String msg = "Postponed due to already running segment"; repairRunner.updateLastEvent(msg); } - boolean canRepair(RepairSegment segment, String keyspace, JmxProxy coordinator, LazyInitializer> busyHosts) { + boolean canRepair( + RepairSegment segment, + String keyspace, + JmxProxy coordinator, + LazyInitializer> busyHosts) { + Collection allHosts; - if(repairUnit.getIncrementalRepair()) { - // In incremental repairs, only one segment is allowed at once (one segment == the full primary range of one node) - if(repairHasSegmentRunning(segment.getRunId())) { - declineRun(); - return false; - } - if (isRepairRunningOnOneNode(segment)) { - declineRun(); - return false; - } + if (repairUnit.getIncrementalRepair()) { + // In incremental repairs, only one segment is allowed at once (one segment == the full primary range of one node) + if (repairHasSegmentRunning(segment.getRunId())) { + declineRun(); + return false; + } + if (isRepairRunningOnOneNode(segment)) { + declineRun(); + return false; + } - return true; + return true; } try { // when hosts are coming up or going down, this method can throw an @@ -387,109 +433,134 @@ boolean canRepair(RepairSegment segment, String keyspace, JmxProxy coordinator, return false; } String datacenter = coordinator.getDataCenter(); - boolean gotMetricsForAllHostsInDC = true; + boolean gotMetricsForAllHostsInDc = true; boolean gotMetricsForAllHosts = true; - Map dcByHost = Maps.newHashMap(); + Map dcByHost = Maps.newHashMap(); allHosts.forEach(host -> dcByHost.put(host, coordinator.getDataCenter(host))); - List>>> getMetricsTasks = allHosts.stream() - .filter(host -> repairUnit.getDatacenters().isEmpty() || repairUnit.getDatacenters().contains(dcByHost.get(host))) + List>>> getMetricsTasks = allHosts + .stream() + .filter( + host + -> repairUnit.getDatacenters().isEmpty() || repairUnit.getDatacenters().contains(dcByHost.get(host))) .map(host -> getNodeMetrics(host, datacenter, dcByHost.get(host))) .collect(Collectors.toList()); try { - for (Future>> future : METRICS_GRABBER_EXECUTOR.invokeAll(getMetricsTasks)) { - try { - Pair> result = future.get(); - if (!result.getRight().isPresent()) { - // We failed at getting metrics for that node - gotMetricsForAllHosts = false; - if (dcByHost.get(result.getLeft()).equals(datacenter)) { - gotMetricsForAllHostsInDC = false; - } - } else { - NodeMetrics metrics = result.getRight().get(); - int pendingCompactions = metrics.getPendingCompactions(); - if (pendingCompactions > MAX_PENDING_COMPACTIONS) { - LOG.info("SegmentRunner declined to repair segment {} because of too many pending compactions (> {}) on host \"{}\"", - segmentId, MAX_PENDING_COMPACTIONS, metrics.getHostAddress()); - String msg = String.format("Postponed due to pending compactions (%d)", pendingCompactions); - repairRunner.updateLastEvent(msg); - return false; - } - if (metrics.hasRepairRunning()) { - LOG.info("SegmentRunner declined to repair segment {} because one of the hosts ({}) was " - + "already involved in a repair", segmentId, metrics.getHostAddress()); - String msg = "Postponed due to affected hosts already doing repairs"; - repairRunner.updateLastEvent(msg); - handlePotentialStuckRepairs(busyHosts, metrics.getHostAddress()); - return false; - } - } - } catch (InterruptedException | ExecutionException | ConcurrentException e) { - LOG.warn("Failed grabbing metrics from at least one node. Cannot repair segment :'(", e); - gotMetricsForAllHostsInDC = false; + for (Future>> future : METRICS_GRABBER_EXECUTOR.invokeAll(getMetricsTasks)) { + try { + Pair> result = future.get(); + if (!result.getRight().isPresent()) { + // We failed at getting metrics for that node gotMetricsForAllHosts = false; + if (dcByHost.get(result.getLeft()).equals(datacenter)) { + gotMetricsForAllHostsInDc = false; + } + } else { + NodeMetrics metrics = result.getRight().get(); + int pendingCompactions = metrics.getPendingCompactions(); + if (pendingCompactions > MAX_PENDING_COMPACTIONS) { + LOG.info( + "SegmentRunner declined to repair segment {} because of" + + " too many pending compactions (> {}) on host \"{}\"", + segmentId, + MAX_PENDING_COMPACTIONS, + metrics.getHostAddress()); + String msg = String.format("Postponed due to pending compactions (%d)", pendingCompactions); + repairRunner.updateLastEvent(msg); + return false; + } + if (metrics.hasRepairRunning()) { + LOG.info( + "SegmentRunner declined to repair segment {} because one of the hosts ({}) was " + + "already involved in a repair", + segmentId, + metrics.getHostAddress()); + String msg = "Postponed due to affected hosts already doing repairs"; + repairRunner.updateLastEvent(msg); + handlePotentialStuckRepairs(busyHosts, metrics.getHostAddress()); + return false; + } } + } catch (InterruptedException | ExecutionException | ConcurrentException e) { + LOG.warn("Failed grabbing metrics from at least one node. Cannot repair segment :'(", e); + gotMetricsForAllHostsInDc = false; + gotMetricsForAllHosts = false; } + } } catch (InterruptedException e) { LOG.debug("failed grabbing nodes metrics", e); } - if(okToRepairSegment(gotMetricsForAllHostsInDC, gotMetricsForAllHosts, context.config.getDatacenterAvailability())) { + if (okToRepairSegment( + gotMetricsForAllHostsInDc, gotMetricsForAllHosts, context.config.getDatacenterAvailability())) { LOG.info("It is ok to repair segment '{}' on repair run with id '{}'", segment.getId(), segment.getRunId()); return true; } else { - LOG.info("Not ok to repair segment '{}' on repair run with id '{}' because we couldn't get all hosts metrics :'(", - segment.getId(), segment.getRunId()); + LOG.info( + "Not ok to repair segment '{}' on repair run with id '{}' because we couldn't get all hosts metrics :'(", + segment.getId(), + segment.getRunId()); return false; } } @VisibleForTesting - public static boolean okToRepairSegment(boolean allHostsInLocalDC, boolean allHosts, DatacenterAvailability dcAvailability) { - return allHosts || (allHostsInLocalDC && DatacenterAvailability.LOCAL.equals(dcAvailability)); + public static boolean okToRepairSegment( + boolean allHostsInLocalDc, + boolean allHosts, + DatacenterAvailability dcAvailability) { + + return allHosts || (allHostsInLocalDc && DatacenterAvailability.LOCAL.equals(dcAvailability)); } - private void handlePotentialStuckRepairs(LazyInitializer> busyHosts, String hostName) throws ConcurrentException { + private void handlePotentialStuckRepairs(LazyInitializer> busyHosts, String hostName) + throws ConcurrentException { + if (!busyHosts.get().contains(hostName) && context.storage instanceof IDistributedStorage) { - LOG.warn("A host ({}) reported that it is involved in a repair, but there is no record " + LOG.warn( + "A host ({}) reported that it is involved in a repair, but there is no record " + "of any ongoing repair involving the host. Sending command to abort all repairs " - + "on the host.", hostName); - try (JmxProxy hostProxy = context.jmxConnectionFactory.connect(hostName, context.config.getJmxConnectionTimeoutInSeconds())) { + + "on the host.", + hostName); + try (JmxProxy hostProxy + = context.jmxConnectionFactory.connect(hostName, context.config.getJmxConnectionTimeoutInSeconds())) { hostProxy.cancelAllRepairs(); hostProxy.close(); - } catch (Exception e) { + } catch (ReaperException | RuntimeException e) { LOG.debug("failed to cancel repairs on host {}", hostName, e); } } } - Callable>> getNodeMetrics(String hostName, String localDatacenter, String hostDatacenter) { + Callable>> getNodeMetrics( + String hostName, String localDatacenter, String hostDatacenter) { return () -> { LOG.debug("getMetricsForHost {} / {} / {}", hostName, localDatacenter, hostDatacenter); - try (JmxProxy hostProxy = context.jmxConnectionFactory.connect(hostName, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy hostProxy + = context.jmxConnectionFactory.connect(hostName, context.config.getJmxConnectionTimeoutInSeconds())) { int pendingCompactions = hostProxy.getPendingCompactions(); boolean hasRepairRunning = hostProxy.isRepairRunning(); - NodeMetrics metrics = NodeMetrics.builder().withHostAddress(hostName) - .withDatacenter(hostDatacenter) - .withPendingCompactions(pendingCompactions) - .withHasRepairRunning(hasRepairRunning) - .withActiveAnticompactions(0) // for future use - .build(); + NodeMetrics metrics = NodeMetrics.builder() + .withHostAddress(hostName) + .withDatacenter(hostDatacenter) + .withPendingCompactions(pendingCompactions) + .withHasRepairRunning(hasRepairRunning) + .withActiveAnticompactions(0) // for future use + .build(); storeNodeMetrics(metrics); return Pair.of(hostName, Optional.of(metrics)); } catch (RuntimeException | ReaperException e) { LOG.debug("failed to query metrics for host {}, trying to get metrics from storage...", hostName, e); if (!DatacenterAvailability.ALL.equals(context.config.getDatacenterAvailability()) - && !hostDatacenter.equals(localDatacenter)) { + && !hostDatacenter.equals(localDatacenter)) { // We can get metrics for remote datacenters from storage Optional metrics = getNodeMetrics(hostName); if (metrics.isPresent()) { - return Pair.of(hostName, metrics); + return Pair.of(hostName, metrics); } } return Pair.of(hostName, Optional.absent()); @@ -497,16 +568,27 @@ Callable>> getNodeMetrics(String hostName, Str }; } + private Optional getNodeMetrics(String hostName) { + Preconditions.checkState(!DatacenterAvailability.ALL.equals(context.config.getDatacenterAvailability())); + + return context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).getNodeMetrics(hostName) + : Optional.absent(); + } + private boolean isRepairRunningOnOneNode(RepairSegment segment) { - for(RepairSegment segmentInRun:context.storage.getRepairSegmentsForRun(segment.getRunId())) { - try (JmxProxy hostProxy = context.jmxConnectionFactory - .connect(segmentInRun.getCoordinatorHost(), context.config.getJmxConnectionTimeoutInSeconds())) { + for (RepairSegment segmentInRun : context.storage.getRepairSegmentsForRun(segment.getRunId())) { + try (JmxProxy hostProxy = context.jmxConnectionFactory.connect( + segmentInRun.getCoordinatorHost(), context.config.getJmxConnectionTimeoutInSeconds())) { if (hostProxy.isRepairRunning()) { return true; } - } catch (ReaperException e) { - LOG.error("Unreachable node when trying to determine if repair is running on a node. Crossing fingers and continuing...", e); + } catch (ReaperException | JMException e) { + LOG.error( + "Unreachable node when trying to determine if repair is running on a node." + + " Crossing fingers and continuing...", + e); } } @@ -514,55 +596,49 @@ private boolean isRepairRunningOnOneNode(RepairSegment segment) { } private boolean repairHasSegmentRunning(UUID repairRunId) { - Collection segments = context.storage.getRepairSegmentsForRun(repairRunId); - for (RepairSegment segment : segments) { - if (segment.getState() == RepairSegment.State.RUNNING) { - LOG.info("segment '{}' is running on host '{}'", segment.getId(), segment.getCoordinatorHost()); - return true; - } - } - - return false; - } + Collection segments = context.storage.getRepairSegmentsForRun(repairRunId); + for (RepairSegment segment : segments) { + if (segment.getState() == RepairSegment.State.RUNNING) { + LOG.info("segment '{}' is running on host '{}'", segment.getId(), segment.getCoordinatorHost()); + return true; + } + } - private void abort(RepairSegment segment, JmxProxy jmxConnection) { - abort(context, segment, jmxConnection); + return false; } private void storeNodeMetrics(NodeMetrics metrics) { - if (context.storage instanceof IDistributedStorage - && !DatacenterAvailability.ALL.equals(context.config.getDatacenterAvailability())) { - - ((IDistributedStorage)context.storage).storeNodeMetrics(metrics); - } - } - - private Optional getNodeMetrics(String hostName) { - Preconditions.checkState(!DatacenterAvailability.ALL.equals(context.config.getDatacenterAvailability())); + if (context.storage instanceof IDistributedStorage + && !DatacenterAvailability.ALL.equals(context.config.getDatacenterAvailability())) { - return context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).getNodeMetrics(hostName) - : Optional.absent(); + ((IDistributedStorage) context.storage).storeNodeMetrics(metrics); + } } /** - * Called when there is an event coming either from JMX or this runner regarding on-going - * repairs. + * Called when there is an event coming either from JMX or this runner regarding on-going repairs. * * @param repairNumber repair sequence number, obtained when triggering a repair - * @param status new status of the repair - * @param message additional information about the repair + * @param status new status of the repair + * @param message additional information about the repair */ @Override - public void handle(int repairNumber, Optional status, Optional progress, String message) { + public void handle( + int repairNumber, + Optional status, + Optional progress, + String message) { + final RepairSegment segment = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); Thread.currentThread().setName(clusterName + ":" + segment.getRunId() + ":" + segmentId); LOG.debug( "handle called for repairCommandId {}, outcome {} / {} and message: {}", - repairNumber, status, progress, message); + repairNumber, + status, + progress, + message); if (repairNumber != commandId) { - LOG.debug("Handler for command id {} not handling message with number {}", - commandId, repairNumber); + LOG.debug("Handler for command id {} not handling message with number {}", commandId, repairNumber); return; } @@ -572,141 +648,177 @@ public void handle(int repairNumber, Optional status RepairSegment currentSegment = context.storage.getRepairSegment(repairRunner.getRepairRunId(), segmentId).get(); // See status explanations at: https://wiki.apache.org/cassandra/RepairAsyncAPI // Old repair API – up to Cassandra-2.1.x - if(status.isPresent()) { - switch (status.get()) { - case STARTED: - try { - if (renewLead()) { - DateTime now = DateTime.now(); - - context.storage.updateRepairSegment(currentSegment.with() - .state(RepairSegment.State.RUNNING) - .startTime(now) - .build(segmentId)); - - LOG.debug("updated segment {} with state {}", segmentId, RepairSegment.State.RUNNING); - break; - } - } catch (AssertionError er) { - // ignore. segment repair has since timed out. - } - segmentFailed.set(true); - break; + if (status.isPresent()) { + failOutsideSynchronizedBlock = handleJmxNotificationForCassandra21( + status, + currentSegment, + repairNumber, + failOutsideSynchronizedBlock, + progress); + } + // New repair API – Cassandra-2.2 onwards + if (progress.isPresent()) { + failOutsideSynchronizedBlock = handleJmxNotificationForCassandra22( + progress, + currentSegment, + repairNumber, + failOutsideSynchronizedBlock); + } + } - case SESSION_SUCCESS: - try { - if (segmentFailed.get()) { - LOG.debug( - "Got SESSION_SUCCESS for segment with id '{}' and repair number '{}', but it had already timed out", - segmentId, repairNumber); - } else if (renewLead()) { - LOG.debug("repair session succeeded for segment with id '{}' and repair number '{}'", segmentId, repairNumber); + if (failOutsideSynchronizedBlock) { + if (takeLead() || renewLead()) { + try { + postponeCurrentSegment(); + tryClearSnapshots(message); + } finally { + // if someone else does hold the lease, ie renewLead(..) was true, + // then their writes to repair_run table and any call to releaseLead(..) will throw an exception + releaseLead(); + } + } + } + } - context.storage.updateRepairSegment(currentSegment.with() - .state(RepairSegment.State.DONE) - .endTime(DateTime.now()) - .build(segmentId)); + private boolean handleJmxNotificationForCassandra22( + Optional progress, + RepairSegment currentSegment, + int repairNumber, + boolean failOutsideSynchronizedBlock) { - break; - } - } catch (AssertionError er) { - // ignore. segment repair has since timed out. - } - segmentFailed.set(true); - break; + switch (progress.get()) { + case START: + try { + if (renewLead()) { + DateTime now = DateTime.now(); - case SESSION_FAILED: - LOG.warn("repair session failed for segment with id '{}' and repair number '{}'", - segmentId, repairNumber); - failOutsideSynchronizedBlock = true; - break; + context.storage.updateRepairSegment( + currentSegment.with().state(RepairSegment.State.RUNNING).startTime(now).build(segmentId)); - case FINISHED: - // This gets called through the JMX proxy at the end - // regardless of succeeded or failed sessions. - LOG.debug("repair session finished for segment with id '{}' and repair number '{}'", - segmentId, repairNumber); - condition.signalAll(); + LOG.debug("updated segment {} with state {}", segmentId, RepairSegment.State.RUNNING); break; + } + } catch (AssertionError er) { + // ignore. segment repair has since timed out. } - } - // New repair API – Cassandra-2.2 onwards - if(progress.isPresent()) { - switch (progress.get()) { - case START: - try { - if (renewLead()) { - DateTime now = DateTime.now(); - - context.storage.updateRepairSegment(currentSegment.with() - .state(RepairSegment.State.RUNNING) - .startTime(now) - .build(segmentId)); - - LOG.debug("updated segment {} with state {}", segmentId, RepairSegment.State.RUNNING); - break; - } - } catch (AssertionError er) { - // ignore. segment repair has since timed out. - } - segmentFailed.set(true); - break; + segmentFailed.set(true); + break; - case SUCCESS: - try { - if (segmentFailed.get()) { - LOG.debug( - "Got SUCCESS for segment with id '{}' and repair number '{}', but it had already timed out", - segmentId, repairNumber); - } else if (renewLead()) { - LOG.debug("repair session succeeded for segment with id '{}' and repair number '{}'", segmentId, repairNumber); - - context.storage.updateRepairSegment(currentSegment.with() - .state(RepairSegment.State.DONE) - .endTime(DateTime.now()) - .build(segmentId)); - break; - } - } catch (AssertionError er) { - // ignore. segment repair has since timed out. - } - segmentFailed.set(true); + case SUCCESS: + try { + if (segmentFailed.get()) { + LOG.debug( + "Got SUCCESS for segment with id '{}' and repair number '{}', but it had already timed out", + segmentId, + repairNumber); + } else if (renewLead()) { + LOG.debug( + "repair session succeeded for segment with id '{}' and repair number '{}'", + segmentId, + repairNumber); + + context.storage.updateRepairSegment( + currentSegment.with().state(RepairSegment.State.DONE).endTime(DateTime.now()).build(segmentId)); break; + } + } catch (AssertionError er) { + // ignore. segment repair has since timed out. + } + segmentFailed.set(true); + break; + + case ERROR: + case ABORT: + LOG.warn("repair session failed for segment with id '{}' and repair number '{}'", segmentId, repairNumber); + failOutsideSynchronizedBlock = true; + break; + + case COMPLETE: + // This gets called through the JMX proxy at the end + // regardless of succeeded or failed sessions. + LOG.debug( + "repair session finished for segment with id '{}' and repair number '{}'", segmentId, repairNumber); + condition.signalAll(); + break; + default: + LOG.debug( + "Unidentified progressStatus {} for segment with id '{}' and repair number '{}'", + progress.get(), + segmentId, + repairNumber); + } + return failOutsideSynchronizedBlock; + } - case ERROR: - case ABORT: - LOG.warn("repair session failed for segment with id '{}' and repair number '{}'", - segmentId, repairNumber); - failOutsideSynchronizedBlock = true; - break; + private boolean handleJmxNotificationForCassandra21( + Optional status, + RepairSegment currentSegment, + int repairNumber, + boolean failOutsideSynchronizedBlock, + Optional progress) { + + switch (status.get()) { + case STARTED: + try { + if (renewLead()) { + DateTime now = DateTime.now(); - case COMPLETE: - // This gets called through the JMX proxy at the end - // regardless of succeeded or failed sessions. - LOG.debug("repair session finished for segment with id '{}' and repair number '{}'", - segmentId, repairNumber); - condition.signalAll(); + context.storage.updateRepairSegment( + currentSegment.with().state(RepairSegment.State.RUNNING).startTime(now).build(segmentId)); + + LOG.debug("updated segment {} with state {}", segmentId, RepairSegment.State.RUNNING); break; - default: - LOG.debug("Unidentified progressStatus {} for segment with id '{}' and repair number '{}'", - progress.get(), segmentId, repairNumber); - break; + } + } catch (AssertionError er) { + // ignore. segment repair has since timed out. } - } - } + segmentFailed.set(true); + break; - if (failOutsideSynchronizedBlock) { - if (takeLead() || renewLead()) { - try { - postponeCurrentSegment(); - tryClearSnapshots(message); - } finally { - // if someone else does hold the lease, ie renewLead(..) was true, - // then their writes to repair_run table and any call to releaseLead(..) will throw an exception - releaseLead(); - } - } + case SESSION_SUCCESS: + try { + if (segmentFailed.get()) { + LOG.debug( + "Got SESSION_SUCCESS for segment with id '{}' and repair number '{}', but it had already timed out", + segmentId, + repairNumber); + } else if (renewLead()) { + LOG.debug( + "repair session succeeded for segment with id '{}' and repair number '{}'", + segmentId, + repairNumber); + + context.storage.updateRepairSegment( + currentSegment.with().state(RepairSegment.State.DONE).endTime(DateTime.now()).build(segmentId)); + + break; + } + } catch (AssertionError er) { + // ignore. segment repair has since timed out. + } + segmentFailed.set(true); + break; + + case SESSION_FAILED: + LOG.warn("repair session failed for segment with id '{}' and repair number '{}'", segmentId, repairNumber); + failOutsideSynchronizedBlock = true; + break; + + case FINISHED: + // This gets called through the JMX proxy at the end + // regardless of succeeded or failed sessions. + LOG.debug( + "repair session finished for segment with id '{}' and repair number '{}'", segmentId, repairNumber); + condition.signalAll(); + break; + default: + LOG.debug( + "Unidentified progressStatus {} for segment with id '{}' and repair number '{}'", + progress.get(), + segmentId, + repairNumber); } + return failOutsideSynchronizedBlock; } /** @@ -718,14 +830,18 @@ protected void tryClearSnapshots(String message) { String repairId = parseRepairId(message); if (repairId != null) { for (String involvedNode : potentialCoordinators) { - try (JmxProxy jmx = new JmxConnectionFactory() - .connect(involvedNode, context.config.getJmxConnectionTimeoutInSeconds())) { + try (JmxProxy jmx + = new JmxConnectionFactory().connect(involvedNode, context.config.getJmxConnectionTimeoutInSeconds())) { // there is no way of telling if the snapshot was cleared or not :( jmx.clearSnapshot(repairId, keyspace); jmx.close(); } catch (ReaperException e) { - LOG.warn("Failed to clear snapshot after failed session for host {}, keyspace {}: {}", - involvedNode, keyspace, e.getMessage(), e); + LOG.warn( + "Failed to clear snapshot after failed session for host {}, keyspace {}: {}", + involvedNode, + keyspace, + e.getMessage(), + e); } } } @@ -757,54 +873,60 @@ long intensityBasedDelayMillis(double intensity) { LOG.debug("Scheduling next runner run() with delay {} ms", delay); int nbRunningReapers = countRunningReapers(); LOG.debug("Concurrent reaper instances : {}", nbRunningReapers); - return delay*nbRunningReapers; + return delay * nbRunningReapers; } else { - LOG.error("Segment {} returned with startTime {} and endTime {}. This should not happen." - + "Intensity cannot apply, so next run will start immediately.", - repairSegment.getId(), repairSegment.getStartTime(), repairSegment.getEndTime()); + LOG.error( + "Segment {} returned with startTime {} and endTime {}. This should not happen." + + "Intensity cannot apply, so next run will start immediately.", + repairSegment.getId(), + repairSegment.getStartTime(), + repairSegment.getEndTime()); return 0; } } - private boolean takeLead() { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "takeLead")).time()) { + private boolean takeLead() { + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "takeLead")).time()) { - boolean result = context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).takeLead(leaderElectionId) - : true; + boolean result = context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).takeLead(leaderElectionId) + : true; - if (!result) { - context.metricRegistry.counter(MetricRegistry.name(SegmentRunner.class, "takeLead", "failed")).inc(); - } - return result; - } + if (!result) { + context.metricRegistry.counter(MetricRegistry.name(SegmentRunner.class, "takeLead", "failed")).inc(); + } + return result; } + } - private boolean renewLead() { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "renewLead")).time()) { + private boolean renewLead() { + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "renewLead")).time()) { - boolean result = context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).renewLead(leaderElectionId) - : true; + boolean result = context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).renewLead(leaderElectionId) + : true; - if (!result) { - context.metricRegistry.counter(MetricRegistry.name(SegmentRunner.class, "renewLead", "failed")).inc(); - } - return result; - } + if (!result) { + context.metricRegistry.counter(MetricRegistry.name(SegmentRunner.class, "renewLead", "failed")).inc(); + } + return result; } + } - private void releaseLead() { - try (Timer.Context cxt = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "releaseLead")).time()) { - if (context.storage instanceof IDistributedStorage) { - ((IDistributedStorage)context.storage).releaseLead(leaderElectionId); - } - } + private void releaseLead() { + try (Timer.Context cx + = context.metricRegistry.timer(MetricRegistry.name(SegmentRunner.class, "releaseLead")).time()) { + if (context.storage instanceof IDistributedStorage) { + ((IDistributedStorage) context.storage).releaseLead(leaderElectionId); + } } + } - private int countRunningReapers() { - return context.storage instanceof IDistributedStorage - ? ((IDistributedStorage)context.storage).countRunningReapers() - : 1; - } + private int countRunningReapers() { + return context.storage instanceof IDistributedStorage + ? ((IDistributedStorage) context.storage).countRunningReapers() + : 1; + } } diff --git a/src/server/src/main/java/com/spotify/reaper/service/SimpleCondition.java b/src/server/src/main/java/com/spotify/reaper/service/SimpleCondition.java index 011e7fa5a..c65718a2e 100644 --- a/src/server/src/main/java/com/spotify/reaper/service/SimpleCondition.java +++ b/src/server/src/main/java/com/spotify/reaper/service/SimpleCondition.java @@ -1,11 +1,7 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * 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 * @@ -15,6 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.service; import java.util.Date; @@ -26,57 +23,57 @@ // _after_ signal(), it will work as desired.) final class SimpleCondition implements Condition { - private boolean set; + private boolean set; - @Override - public synchronized void await() throws InterruptedException { - while (!set) { - wait(); - } + @Override + public synchronized void await() throws InterruptedException { + while (!set) { + wait(); } + } - public synchronized void reset() { - set = false; + @Override + public synchronized boolean await(long time, TimeUnit unit) throws InterruptedException { + long start = System.nanoTime(); + long timeout = unit.toNanos(time); + long elapsed; + while (!set && (elapsed = System.nanoTime() - start) < timeout) { + TimeUnit.NANOSECONDS.timedWait(this, timeout - elapsed); } + return set; + } - @Override - public synchronized boolean await(long time, TimeUnit unit) throws InterruptedException { - long start = System.nanoTime(); - long timeout = unit.toNanos(time); - long elapsed; - while (!set && (elapsed = System.nanoTime() - start) < timeout) { - TimeUnit.NANOSECONDS.timedWait(this, timeout - elapsed); - } - return set; - } + public synchronized void reset() { + set = false; + } - @Override - public void signal() { - throw new UnsupportedOperationException(); - } + @Override + public void signal() { + throw new UnsupportedOperationException(); + } - @Override - public synchronized void signalAll() { - set = true; - notifyAll(); - } + @Override + public synchronized void signalAll() { + set = true; + notifyAll(); + } - public synchronized boolean isSignaled() { - return set; - } + public synchronized boolean isSignaled() { + return set; + } - @Override - public void awaitUninterruptibly() { - throw new UnsupportedOperationException(); - } + @Override + public void awaitUninterruptibly() { + throw new UnsupportedOperationException(); + } - @Override - public long awaitNanos(long nanosTimeout) throws InterruptedException { - throw new UnsupportedOperationException(); - } + @Override + public long awaitNanos(long nanosTimeout) throws InterruptedException { + throw new UnsupportedOperationException(); + } - @Override - public boolean awaitUntil(Date deadline) throws InterruptedException { - throw new UnsupportedOperationException(); - } + @Override + public boolean awaitUntil(Date deadline) throws InterruptedException { + throw new UnsupportedOperationException(); + } } diff --git a/src/server/src/main/java/com/spotify/reaper/service/package-info.java b/src/server/src/main/java/com/spotify/reaper/service/package-info.java new file mode 100644 index 000000000..9c288a361 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/service/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.service; diff --git a/src/server/src/main/java/com/spotify/reaper/storage/CassandraStorage.java b/src/server/src/main/java/com/spotify/reaper/storage/CassandraStorage.java index ca4a93eef..18bea458e 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/CassandraStorage.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/CassandraStorage.java @@ -1,5 +1,37 @@ +/* + * 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.spotify.reaper.storage; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperApplicationConfiguration; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.NodeMetrics; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairRun.Builder; +import com.spotify.reaper.core.RepairRun.RunState; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairSegment.State; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.view.RepairRunStatus; +import com.spotify.reaper.resources.view.RepairScheduleStatus; +import com.spotify.reaper.service.RepairParameters; +import com.spotify.reaper.service.RingRange; +import com.spotify.reaper.storage.cassandra.DateTimeCodec; +import com.spotify.reaper.storage.cassandra.Migration003; + import java.math.BigInteger; import java.util.Collection; import java.util.Collections; @@ -10,14 +42,6 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import org.apache.cassandra.repair.RepairParallelism; -import org.cognitor.cassandra.migration.Database; -import org.cognitor.cassandra.migration.MigrationRepository; -import org.cognitor.cassandra.migration.MigrationTask; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.datastax.driver.core.BatchStatement; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.ConsistencyLevel; @@ -41,38 +65,30 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplicationConfiguration; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.NodeMetrics; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairRun.Builder; -import com.spotify.reaper.core.RepairRun.RunState; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairSegment.State; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.view.RepairRunStatus; -import com.spotify.reaper.resources.view.RepairScheduleStatus; -import com.spotify.reaper.service.RepairParameters; -import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.storage.cassandra.DateTimeCodec; -import com.spotify.reaper.storage.cassandra.Migration003; - import io.dropwizard.setup.Environment; +import org.apache.cassandra.repair.RepairParallelism; +import org.cognitor.cassandra.migration.Database; +import org.cognitor.cassandra.migration.MigrationRepository; +import org.cognitor.cassandra.migration.MigrationTask; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import systems.composable.dropwizard.cassandra.CassandraFactory; import systems.composable.dropwizard.cassandra.retry.RetryPolicyFactory; public final class CassandraStorage implements IStorage, IDistributedStorage { - private static final Logger LOG = LoggerFactory.getLogger(CassandraStorage.class); - com.datastax.driver.core.Cluster cassandra = null; - Session session; /* Simple stmts */ private static final String SELECT_CLUSTER = "SELECT * FROM cluster"; private static final String SELECT_REPAIR_SCHEDULE = "SELECT * FROM repair_schedule_v1"; private static final String SELECT_REPAIR_UNIT = "SELECT * FROM repair_unit_v1"; + private static final Logger LOG = LoggerFactory.getLogger(CassandraStorage.class); + + private final com.datastax.driver.core.Cluster cassandra; + private final Session session; + + /* prepared stmts */ private PreparedStatement insertClusterPrepStmt; private PreparedStatement getClusterPrepStmt; @@ -109,19 +125,20 @@ public final class CassandraStorage implements IStorage, IDistributedStorage { public CassandraStorage(ReaperApplicationConfiguration config, Environment environment) { CassandraFactory cassandraFactory = config.getCassandraFactory(); - // all INSERT and DELETE stmt prepared in this class are idempotent + // all INSERT and DELETE stmt prepared in this class are idempoten if (cassandraFactory.getQueryOptions().isPresent() - && ConsistencyLevel.LOCAL_ONE != cassandraFactory.getQueryOptions().get().getConsistencyLevel()) { - LOG.warn("Customization of cassandra's queryOptions is not supported and will be overridden"); + && ConsistencyLevel.LOCAL_ONE != cassandraFactory.getQueryOptions().get().getConsistencyLevel()) { + LOG.warn("Customization of cassandra's queryOptions is not supported and will be overridden"); } cassandraFactory.setQueryOptions(java.util.Optional.of(new QueryOptions().setDefaultIdempotence(true))); if (cassandraFactory.getRetryPolicy().isPresent()) { - LOG.warn("Customization of cassandra's retry policy is not supported and will be overridden"); + LOG.warn("Customization of cassandra's retry policy is not supported and will be overridden"); } cassandraFactory.setRetryPolicy(java.util.Optional.of((RetryPolicyFactory) () -> new RetryPolicyImpl())); cassandra = cassandraFactory.build(environment); - if(config.getActivateQueryLogger()) + if (config.getActivateQueryLogger()) { cassandra.register(QueryLogger.builder().build()); + } CodecRegistry codecRegistry = cassandra.getConfiguration().getCodecRegistry(); codecRegistry.register(new DateTimeCodec()); @@ -136,43 +153,102 @@ public CassandraStorage(ReaperApplicationConfiguration config, Environment envir lastHeartBeat = lastHeartBeat.minusMinutes(1); } - private void prepareStatements(){ - insertClusterPrepStmt = session.prepare("INSERT INTO cluster(name, partitioner, seed_hosts) values(?, ?, ?)").setConsistencyLevel(ConsistencyLevel.QUORUM); - getClusterPrepStmt = session.prepare("SELECT * FROM cluster WHERE name = ?").setConsistencyLevel(ConsistencyLevel.QUORUM).setRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE); + private void prepareStatements() { + insertClusterPrepStmt = session + .prepare("INSERT INTO cluster(name, partitioner, seed_hosts) values(?, ?, ?)") + .setConsistencyLevel(ConsistencyLevel.QUORUM); + getClusterPrepStmt = session + .prepare("SELECT * FROM cluster WHERE name = ?") + .setConsistencyLevel(ConsistencyLevel.QUORUM) + .setRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE); deleteClusterPrepStmt = session.prepare("DELETE FROM cluster WHERE name = ?"); - insertRepairRunPrepStmt = session.prepare("INSERT INTO repair_run(id, cluster_name, repair_unit_id, cause, owner, state, creation_time, start_time, end_time, pause_time, intensity, last_event, segment_count, repair_parallelism) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").setConsistencyLevel(ConsistencyLevel.QUORUM); - insertRepairRunClusterIndexPrepStmt = session.prepare("INSERT INTO repair_run_by_cluster(cluster_name, id) values(?, ?)"); - insertRepairRunUnitIndexPrepStmt = session.prepare("INSERT INTO repair_run_by_unit(repair_unit_id, id) values(?, ?)"); - getRepairRunPrepStmt = session.prepare("SELECT id,cluster_name,repair_unit_id,cause,owner,state,creation_time,start_time,end_time,pause_time,intensity,last_event,segment_count,repair_parallelism FROM repair_run WHERE id = ? LIMIT 1").setConsistencyLevel(ConsistencyLevel.QUORUM); + insertRepairRunPrepStmt = session + .prepare( + "INSERT INTO repair_run(id, cluster_name, repair_unit_id, cause, owner, state, creation_time, " + + "start_time, end_time, pause_time, intensity, last_event, segment_count, repair_parallelism) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + .setConsistencyLevel(ConsistencyLevel.QUORUM); + insertRepairRunClusterIndexPrepStmt + = session.prepare("INSERT INTO repair_run_by_cluster(cluster_name, id) values(?, ?)"); + insertRepairRunUnitIndexPrepStmt + = session.prepare("INSERT INTO repair_run_by_unit(repair_unit_id, id) values(?, ?)"); + getRepairRunPrepStmt = session + .prepare( + "SELECT id,cluster_name,repair_unit_id,cause,owner,state,creation_time,start_time,end_time," + + "pause_time,intensity,last_event,segment_count,repair_parallelism " + + "FROM repair_run WHERE id = ? LIMIT 1") + .setConsistencyLevel(ConsistencyLevel.QUORUM); getRepairRunForClusterPrepStmt = session.prepare("SELECT * FROM repair_run_by_cluster WHERE cluster_name = ?"); getRepairRunForUnitPrepStmt = session.prepare("SELECT * FROM repair_run_by_unit WHERE repair_unit_id = ?"); deleteRepairRunPrepStmt = session.prepare("DELETE FROM repair_run WHERE id = ?"); - deleteRepairRunByClusterPrepStmt = session.prepare("DELETE FROM repair_run_by_cluster WHERE id = ? and cluster_name = ?"); - deleteRepairRunByUnitPrepStmt = session.prepare("DELETE FROM repair_run_by_unit WHERE id = ? and repair_unit_id= ?"); + deleteRepairRunByClusterPrepStmt + = session.prepare("DELETE FROM repair_run_by_cluster WHERE id = ? and cluster_name = ?"); + deleteRepairRunByUnitPrepStmt = session.prepare("DELETE FROM repair_run_by_unit " + + "WHERE id = ? and repair_unit_id= ?"); insertRepairUnitPrepStmt = session.prepare( - "INSERT INTO repair_unit_v1(id, cluster_name, keyspace_name, column_families, incremental_repair, nodes, datacenters) VALUES(?, ?, ?, ?, ?, ?, ?)"); + "INSERT INTO repair_unit_v1(id, cluster_name, keyspace_name, column_families, " + + "incremental_repair, nodes, datacenters) VALUES(?, ?, ?, ?, ?, ?, ?)"); getRepairUnitPrepStmt = session.prepare("SELECT * FROM repair_unit_v1 WHERE id = ?"); - insertRepairSegmentPrepStmt = session.prepare("INSERT INTO repair_run(id, segment_id, repair_unit_id, start_token, end_token, segment_state, coordinator_host, segment_start_time, segment_end_time, fail_count) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM); - getRepairSegmentPrepStmt = session.prepare("SELECT id,repair_unit_id,segment_id,start_token,end_token,segment_state,coordinator_host,segment_start_time,segment_end_time,fail_count FROM repair_run WHERE id = ? and segment_id = ?").setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM); - getRepairSegmentsByRunIdPrepStmt = session.prepare("SELECT id,repair_unit_id,segment_id,start_token,end_token,segment_state,coordinator_host,segment_start_time,segment_end_time,fail_count FROM repair_run WHERE id = ?"); - insertRepairSchedulePrepStmt = session.prepare("INSERT INTO repair_schedule_v1(id, repair_unit_id, state, days_between, next_activation, run_history, segment_count, repair_parallelism, intensity, creation_time, owner, pause_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").setConsistencyLevel(ConsistencyLevel.QUORUM); - getRepairSchedulePrepStmt = session.prepare("SELECT * FROM repair_schedule_v1 WHERE id = ?").setConsistencyLevel(ConsistencyLevel.QUORUM); - insertRepairScheduleByClusterAndKsPrepStmt = session.prepare("INSERT INTO repair_schedule_by_cluster_and_keyspace(cluster_name, keyspace_name, repair_schedule_id) VALUES(?, ?, ?)"); - getRepairScheduleByClusterAndKsPrepStmt = session.prepare("SELECT repair_schedule_id FROM repair_schedule_by_cluster_and_keyspace WHERE cluster_name = ? and keyspace_name = ?"); + insertRepairSegmentPrepStmt = session + .prepare( + "INSERT INTO repair_run(id, segment_id, repair_unit_id, start_token, end_token, segment_state, " + + "coordinator_host, segment_start_time, segment_end_time, fail_count)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM); + getRepairSegmentPrepStmt = session + .prepare( + "SELECT id,repair_unit_id,segment_id,start_token,end_token,segment_state,coordinator_host," + + "segment_start_time,segment_end_time,fail_count FROM repair_run WHERE id = ? and segment_id = ?") + .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM); + getRepairSegmentsByRunIdPrepStmt = session.prepare( + "SELECT id,repair_unit_id,segment_id,start_token,end_token,segment_state,coordinator_host,segment_start_time," + + "segment_end_time,fail_count FROM repair_run WHERE id = ?"); + insertRepairSchedulePrepStmt = session + .prepare( + "INSERT INTO repair_schedule_v1(id, repair_unit_id, state, days_between, next_activation, run_history, " + + "segment_count, repair_parallelism, intensity, creation_time, owner, pause_time) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + .setConsistencyLevel(ConsistencyLevel.QUORUM); + getRepairSchedulePrepStmt + = session.prepare("SELECT * FROM repair_schedule_v1 WHERE id = ?").setConsistencyLevel(ConsistencyLevel.QUORUM); + insertRepairScheduleByClusterAndKsPrepStmt = session.prepare( + "INSERT INTO repair_schedule_by_cluster_and_keyspace(cluster_name, keyspace_name, repair_schedule_id)" + + " VALUES(?, ?, ?)"); + getRepairScheduleByClusterAndKsPrepStmt = session.prepare( + "SELECT repair_schedule_id FROM repair_schedule_by_cluster_and_keyspace " + + "WHERE cluster_name = ? and keyspace_name = ?"); deleteRepairSchedulePrepStmt = session.prepare("DELETE FROM repair_schedule_v1 WHERE id = ?"); - deleteRepairScheduleByClusterAndKsPrepStmt = session.prepare("DELETE FROM repair_schedule_by_cluster_and_keyspace WHERE cluster_name = ? and keyspace_name = ? and repair_schedule_id = ?"); - takeLeadPrepStmt = session.prepare("INSERT INTO leader(leader_id, reaper_instance_id, reaper_instance_host, last_heartbeat) VALUES(?, ?, ?, dateof(now())) IF NOT EXISTS").setIdempotent(false); - renewLeadPrepStmt = session.prepare("UPDATE leader SET reaper_instance_id = ?, reaper_instance_host = ?, last_heartbeat = dateof(now()) WHERE leader_id = ? IF reaper_instance_id = ?").setIdempotent(false); + deleteRepairScheduleByClusterAndKsPrepStmt = session.prepare( + "DELETE FROM repair_schedule_by_cluster_and_keyspace " + + "WHERE cluster_name = ? and keyspace_name = ? and repair_schedule_id = ?"); + takeLeadPrepStmt = session + .prepare( + "INSERT INTO leader(leader_id, reaper_instance_id, reaper_instance_host, last_heartbeat) " + + "VALUES(?, ?, ?, dateof(now())) IF NOT EXISTS") + .setIdempotent(false); + renewLeadPrepStmt = session + .prepare( + "UPDATE leader SET reaper_instance_id = ?, reaper_instance_host = ?, last_heartbeat = dateof(now()) " + + "WHERE leader_id = ? IF reaper_instance_id = ?") + .setIdempotent(false); releaseLeadPrepStmt = session.prepare("DELETE FROM leader WHERE leader_id = ? IF reaper_instance_id = ?"); getRunningReapersCountPrepStmt = session.prepare("SELECT count(*) as nb_reapers FROM running_reapers"); - saveHeartbeatPrepStmt = session.prepare("INSERT INTO running_reapers(reaper_instance_id, reaper_instance_host, last_heartbeat) VALUES(?,?,dateof(now()))").setIdempotent(false); - storeNodeMetricsPrepStmt = session.prepare("INSERT INTO node_metrics (host_address, datacenter, pending_compactions, has_repair_running, active_anticompactions) VALUES(?, ?, ?, ?, ?)").setIdempotent(false); + saveHeartbeatPrepStmt = session + .prepare( + "INSERT INTO running_reapers(reaper_instance_id, reaper_instance_host, last_heartbeat)" + + " VALUES(?,?,dateof(now()))") + .setIdempotent(false); + storeNodeMetricsPrepStmt = session + .prepare( + "INSERT INTO node_metrics (host_address, datacenter, pending_compactions, " + + "has_repair_running, active_anticompactions) VALUES(?, ?, ?, ?, ?)") + .setIdempotent(false); getNodeMetricsPrepStmt = session.prepare("SELECT * FROM node_metrics WHERE host_address = ?"); } @Override public boolean isStorageConnected() { - return session!=null && !session.isClosed(); + return session != null && !session.isClosed(); } @Override @@ -181,8 +257,10 @@ public Collection getClusters() { Statement stmt = new SimpleStatement(SELECT_CLUSTER); stmt.setIdempotent(Boolean.TRUE); ResultSet clusterResults = session.execute(stmt); - for(Row cluster:clusterResults){ - clusters.add(new Cluster(cluster.getString("name"), cluster.getString("partitioner"), cluster.getSet("seed_hosts", String.class))); + for (Row cluster : clusterResults) { + clusters.add( + new Cluster( + cluster.getString("name"), cluster.getString("partitioner"), cluster.getSet("seed_hosts", String.class))); } return clusters; @@ -201,12 +279,12 @@ public boolean updateCluster(Cluster newCluster) { @Override public Optional getCluster(String clusterName) { - Row r = session.execute(getClusterPrepStmt.bind(clusterName)).one(); + Row row = session.execute(getClusterPrepStmt.bind(clusterName)).one(); - return r != null - ? Optional.fromNullable( - new Cluster(r.getString("name"), r.getString("partitioner"), r.getSet("seed_hosts", String.class))) - : Optional.absent(); + return row != null + ? Optional.fromNullable( + new Cluster(row.getString("name"), row.getString("partitioner"), row.getSet("seed_hosts", String.class))) + : Optional.absent(); } @Override @@ -221,7 +299,8 @@ public RepairRun addRepairRun(Builder repairRun, Collection futures = Lists.newArrayList(); - repairRunBatch.add(insertRepairRunPrepStmt.bind( + repairRunBatch.add( + insertRepairRunPrepStmt.bind( newRepairRun.getId(), newRepairRun.getClusterName(), newRepairRun.getRepairUnitId(), @@ -237,41 +316,47 @@ public RepairRun addRepairRun(Builder repairRun, Collection getRepairRun(UUID id) { RepairRun repairRun = null; Row repairRunResult = session.execute(getRepairRunPrepStmt.bind(id)).one(); - if(repairRunResult != null){ - repairRun = buildRepairRunFromRow(repairRunResult, id); + if (repairRunResult != null) { + try { + repairRun = buildRepairRunFromRow(repairRunResult, id); + } catch (RuntimeException ignore) { + // has been since deleted, but zombie segments has been re-inserted + } } - return Optional.fromNullable(repairRun); } @@ -307,7 +395,7 @@ public Collection getRepairRunsForCluster(String clusterName) { // Grab all ids for the given cluster name Collection repairRunIds = getRepairRunIdsForCluster(clusterName); // Grab repair runs asynchronously for all the ids returned by the index table - for(UUID repairRunId:repairRunIds){ + for (UUID repairRunId : repairRunIds) { repairRunFutures.add(session.executeAsync(getRepairRunPrepStmt.bind(repairRunId))); } @@ -316,36 +404,29 @@ public Collection getRepairRunsForCluster(String clusterName) { @Override public Collection getRepairRunsForUnit(UUID repairUnitId) { - Collection repairRuns = Lists.newArrayList(); List repairRunFutures = Lists.newArrayList(); // Grab all ids for the given cluster name ResultSet repairRunIds = session.execute(getRepairRunForUnitPrepStmt.bind(repairUnitId)); // Grab repair runs asynchronously for all the ids returned by the index table - for(Row repairRunId:repairRunIds){ + for (Row repairRunId : repairRunIds) { repairRunFutures.add(session.executeAsync(getRepairRunPrepStmt.bind(repairRunId.getUUID("id")))); } - repairRuns = getRepairRunsAsync(repairRunFutures); - - return repairRuns; + return getRepairRunsAsync(repairRunFutures); } - /** - * Create a collection of RepairRun objects out of a list of ResultSetFuture. - * Used to handle async queries on the repair_run table with a list of ids. - * - * @param repairRunFutures - * @return + * Create a collection of RepairRun objects out of a list of ResultSetFuture. Used to handle async queries on the + * repair_run table with a list of ids. */ - private Collection getRepairRunsAsync(List repairRunFutures){ + private Collection getRepairRunsAsync(List repairRunFutures) { Collection repairRuns = Lists.newArrayList(); - for(ResultSetFuture repairRunFuture:repairRunFutures){ + for (ResultSetFuture repairRunFuture : repairRunFutures) { Row repairRunResult = repairRunFuture.getUninterruptibly().one(); - if(repairRunResult != null){ + if (repairRunResult != null) { RepairRun repairRun = buildRepairRunFromRow(repairRunResult, repairRunResult.getUUID("id")); repairRuns.add(repairRun); } @@ -358,12 +439,11 @@ private Collection getRepairRunsAsync(List repairRun public Collection getRepairRunsWithState(RunState runState) { Set repairRunsWithState = Sets.newHashSet(); - List> repairRunIds = - getClusters() - .stream() - // Grab all ids for the given cluster name - .map(cluster -> getRepairRunIdsForCluster(cluster.getName())) - .collect(Collectors.toList()); + List> repairRunIds = getClusters() + .stream() + // Grab all ids for the given cluster name + .map(cluster -> getRepairRunIdsForCluster(cluster.getName())) + .collect(Collectors.toList()); for (Collection clusterRepairRunIds : repairRunIds) { repairRunsWithState.addAll(getRepairRunsWithStateForCluster(clusterRepairRunIds, runState)); @@ -373,11 +453,13 @@ public Collection getRepairRunsWithState(RunState runState) { } private Collection getRepairRunsWithStateForCluster( - Collection clusterRepairRunsId, RunState runState) { + Collection clusterRepairRunsId, + RunState runState) { + Collection repairRuns = Sets.newHashSet(); List futures = Lists.newArrayList(); - for (UUID repairRunId:clusterRepairRunsId) { + for (UUID repairRunId : clusterRepairRunsId) { futures.add(session.executeAsync(getRepairRunPrepStmt.bind(repairRunId))); } @@ -388,20 +470,17 @@ private Collection getRepairRunsWithStateForCluster( } } - return repairRuns - .stream() - .filter(repairRun -> repairRun.getRunState() == runState) - .collect(Collectors.toSet()); + return repairRuns.stream().filter(repairRun -> repairRun.getRunState() == runState).collect(Collectors.toSet()); } @Override public Optional deleteRepairRun(UUID id) { Optional repairRun = getRepairRun(id); - if(repairRun.isPresent()){ + if (repairRun.isPresent()) { session.executeAsync(deleteRepairRunByUnitPrepStmt.bind(id, repairRun.get().getRepairUnitId())); session.executeAsync(deleteRepairRunByClusterPrepStmt.bind(id, repairRun.get().getClusterName())); - session.executeAsync(deleteRepairRunPrepStmt.bind(id)); } + session.executeAsync(deleteRepairRunPrepStmt.bind(id)); return repairRun; } @@ -409,8 +488,13 @@ public Optional deleteRepairRun(UUID id) { public RepairUnit addRepairUnit(RepairUnit.Builder newRepairUnit) { RepairUnit repairUnit = newRepairUnit.build(UUIDs.timeBased()); session.execute( - insertRepairUnitPrepStmt.bind(repairUnit.getId(), repairUnit.getClusterName(), repairUnit.getKeyspaceName(), - repairUnit.getColumnFamilies(), repairUnit.getIncrementalRepair(), repairUnit.getNodes(), + insertRepairUnitPrepStmt.bind( + repairUnit.getId(), + repairUnit.getClusterName(), + repairUnit.getKeyspaceName(), + repairUnit.getColumnFamilies(), + repairUnit.getIncrementalRepair(), + repairUnit.getNodes(), repairUnit.getDatacenters())); return repairUnit; } @@ -419,11 +503,15 @@ public RepairUnit addRepairUnit(RepairUnit.Builder newRepairUnit) { public Optional getRepairUnit(UUID id) { RepairUnit repairUnit = null; Row repairUnitRow = session.execute(getRepairUnitPrepStmt.bind(id)).one(); - if(repairUnitRow!=null){ - repairUnit = new RepairUnit.Builder(repairUnitRow.getString("cluster_name"), - repairUnitRow.getString("keyspace_name"), repairUnitRow.getSet("column_families", String.class), - repairUnitRow.getBool("incremental_repair"), repairUnitRow.getSet("nodes", String.class), - repairUnitRow.getSet("datacenters", String.class)).build(id); + if (repairUnitRow != null) { + repairUnit = new RepairUnit.Builder( + repairUnitRow.getString("cluster_name"), + repairUnitRow.getString("keyspace_name"), + repairUnitRow.getSet("column_families", String.class), + repairUnitRow.getBool("incremental_repair"), + repairUnitRow.getSet("nodes", String.class), + repairUnitRow.getSet("datacenters", String.class)) + .build(id); } return Optional.fromNullable(repairUnit); } @@ -431,19 +519,22 @@ public Optional getRepairUnit(UUID id) { @Override public Optional getRepairUnit(String cluster, String keyspace, Set columnFamilyNames) { // brute force again - RepairUnit repairUnit=null; + RepairUnit repairUnit = null; Statement stmt = new SimpleStatement(SELECT_REPAIR_UNIT); stmt.setIdempotent(Boolean.TRUE); ResultSet results = session.execute(stmt); - for(Row repairUnitRow:results){ - if(repairUnitRow.getString("cluster_name").equals(cluster) + for (Row repairUnitRow : results) { + if (repairUnitRow.getString("cluster_name").equals(cluster) && repairUnitRow.getString("keyspace_name").equals(keyspace) - && repairUnitRow.getSet("column_families", String.class).equals(columnFamilyNames)){ - repairUnit = new RepairUnit.Builder(repairUnitRow.getString("cluster_name"), - repairUnitRow.getString("keyspace_name"), repairUnitRow.getSet("column_families", String.class), - repairUnitRow.getBool("incremental_repair"), repairUnitRow.getSet("nodes", String.class), + && repairUnitRow.getSet("column_families", String.class).equals(columnFamilyNames)) { + repairUnit = new RepairUnit.Builder( + repairUnitRow.getString("cluster_name"), + repairUnitRow.getString("keyspace_name"), + repairUnitRow.getSet("column_families", String.class), + repairUnitRow.getBool("incremental_repair"), + repairUnitRow.getSet("nodes", String.class), repairUnitRow.getSet("datacenters", String.class)) - .build(repairUnitRow.getUUID("id")); + .build(repairUnitRow.getUUID("id")); // exit the loop once we find a match break; } @@ -456,14 +547,16 @@ public Optional getRepairUnit(String cluster, String keyspace, Set getRepairSegment(UUID runId, UUID segmentId) { RepairSegment segment = null; Row segmentRow = session.execute(getRepairSegmentPrepStmt.bind(runId, segmentId)).one(); - if(segmentRow != null){ + if (segmentRow != null) { segment = createRepairSegmentFromRow(segmentRow); } @@ -494,8 +587,8 @@ public Collection getRepairSegmentsForRun(UUID runId) { Collection segments = Lists.newArrayList(); // First gather segments ids ResultSet segmentsIdResultSet = session.execute(getRepairSegmentsByRunIdPrepStmt.bind(runId)); - for(Row segmentRow : segmentsIdResultSet) { - segments.add(createRepairSegmentFromRow(segmentRow)); + for (Row segmentRow : segmentsIdResultSet) { + segments.add(createRepairSegmentFromRow(segmentRow)); } return segments; @@ -508,15 +601,19 @@ public Collection getRepairSegmentsForRunInLocalMode(UUID runId, // First gather segments ids ResultSet segmentsResultSet = session.execute(getRepairSegmentsByRunIdPrepStmt.bind(runId)); - segmentsResultSet.forEach(segmentRow -> { - RepairSegment seg = createRepairSegmentFromRow(segmentRow); - RingRange range = new RingRange(seg.getStartToken(), seg.getEndToken()); - localRanges.stream().forEach(localRange -> { - if(localRange.encloses(range)) { - segments.add(seg); - } - }); - }); + segmentsResultSet.forEach( + segmentRow -> { + RepairSegment seg = createRepairSegmentFromRow(segmentRow); + RingRange range = new RingRange(seg.getStartToken(), seg.getEndToken()); + localRanges + .stream() + .forEach( + localRange -> { + if (localRange.encloses(range)) { + segments.add(seg); + } + }); + }); return segments; } @@ -525,12 +622,12 @@ private static boolean segmentIsWithinRange(RepairSegment segment, RingRange ran return range.encloses(new RingRange(segment.getStartToken(), segment.getEndToken())); } - private static RepairSegment createRepairSegmentFromRow(Row segmentRow){ + private static RepairSegment createRepairSegmentFromRow(Row segmentRow) { return new RepairSegment.Builder( - new RingRange( - new BigInteger(segmentRow.getVarint("start_token") +""), - new BigInteger(segmentRow.getVarint("end_token")+"")), - segmentRow.getUUID("repair_unit_id")) + new RingRange( + new BigInteger(segmentRow.getVarint("start_token") + ""), + new BigInteger(segmentRow.getVarint("end_token") + "")), + segmentRow.getUUID("repair_unit_id")) .withRunId(segmentRow.getUUID("id")) .coordinatorHost(segmentRow.getString("coordinator_host")) .endTime(new DateTime(segmentRow.getTimestamp("segment_end_time"))) @@ -540,13 +637,12 @@ private static RepairSegment createRepairSegmentFromRow(Row segmentRow){ .build(segmentRow.getUUID("segment_id")); } - @Override public Optional getNextFreeSegmentInRange(UUID runId, Optional range) { List segments = Lists.newArrayList(getRepairSegmentsForRun(runId)); Collections.shuffle(segments); - for(RepairSegment seg:segments){ + for (RepairSegment seg : segments) { if (seg.getState().equals(State.NOT_STARTED) && withinRange(seg, range)) { return Optional.of(seg); } @@ -561,8 +657,8 @@ public Collection getSegmentsWithState(UUID runId, State segmentS segments.addAll(getRepairSegmentsForRun(runId)); - for(RepairSegment seg:segments){ - if(seg.getState().equals(segmentState)){ + for (RepairSegment seg : segments) { + if (seg.getState().equals(segmentState)) { foundSegments.add(seg); } } @@ -576,11 +672,16 @@ public Collection getOngoingRepairsInCluster(String clusterNam Collection repairRuns = getRepairRunsForCluster(clusterName); - for(RepairRun repairRun:repairRuns){ + for (RepairRun repairRun : repairRuns) { Collection runningSegments = getSegmentsWithState(repairRun.getId(), State.RUNNING); - for(RepairSegment segment:runningSegments){ + for (RepairSegment segment : runningSegments) { Optional repairUnit = getRepairUnit(repairRun.getRepairUnitId()); - repairs.add(new RepairParameters(new RingRange(segment.getStartToken(), segment.getEndToken()), repairUnit.get().getKeyspaceName(), repairUnit.get().getColumnFamilies(), repairRun.getRepairParallelism())); + repairs.add( + new RepairParameters( + new RingRange(segment.getStartToken(), segment.getEndToken()), + repairUnit.get().getKeyspaceName(), + repairUnit.get().getColumnFamilies(), + repairRun.getRepairParallelism())); } } @@ -593,7 +694,7 @@ public Collection getOngoingRepairsInCluster(String clusterNam public Collection getRepairRunIdsForCluster(String clusterName) { Collection repairRunIds = Lists.newArrayList(); ResultSet results = session.execute(getRepairRunForClusterPrepStmt.bind(clusterName)); - for(Row result:results){ + for (Row result : results) { repairRunIds.add(result.getUUID("id")); } @@ -604,7 +705,6 @@ public Collection getRepairRunIdsForCluster(String clusterName) { @Override public int getSegmentAmountForRepairRun(UUID runId) { return getRepairSegmentsForRun(runId).size(); - } @Override @@ -620,19 +720,16 @@ public RepairSchedule addRepairSchedule(com.spotify.reaper.core.RepairSchedule.B return schedule; } - - @Override public Optional getRepairSchedule(UUID repairScheduleId) { Row sched = session.execute(getRepairSchedulePrepStmt.bind(repairScheduleId)).one(); - return sched != null - ? Optional.fromNullable(createRepairScheduleFromRow(sched)) - : Optional.absent(); + return sched != null ? Optional.fromNullable(createRepairScheduleFromRow(sched)) : Optional.absent(); } - private RepairSchedule createRepairScheduleFromRow(Row repairScheduleRow){ - return new RepairSchedule.Builder(repairScheduleRow.getUUID("repair_unit_id"), + private RepairSchedule createRepairScheduleFromRow(Row repairScheduleRow) { + return new RepairSchedule.Builder( + repairScheduleRow.getUUID("repair_unit_id"), RepairSchedule.State.valueOf(repairScheduleRow.getString("state")), repairScheduleRow.getInt("days_between"), new DateTime(repairScheduleRow.getTimestamp("next_activation")), @@ -642,18 +739,17 @@ private RepairSchedule createRepairScheduleFromRow(Row repairScheduleRow){ repairScheduleRow.getDouble("intensity"), new DateTime(repairScheduleRow.getTimestamp("creation_time"))) .owner(repairScheduleRow.getString("owner")) - .pauseTime(new DateTime(repairScheduleRow.getTimestamp("pause_time"))).build(repairScheduleRow.getUUID("id")); - - + .pauseTime(new DateTime(repairScheduleRow.getTimestamp("pause_time"))) + .build(repairScheduleRow.getUUID("id")); } @Override public Collection getRepairSchedulesForCluster(String clusterName) { Collection schedules = Lists.newArrayList(); ResultSet scheduleIds = session.execute(getRepairScheduleByClusterAndKsPrepStmt.bind(clusterName, " ")); - for(Row scheduleId:scheduleIds){ + for (Row scheduleId : scheduleIds) { Optional schedule = getRepairSchedule(scheduleId.getUUID("repair_schedule_id")); - if(schedule.isPresent()){ + if (schedule.isPresent()) { schedules.add(schedule.get()); } } @@ -665,9 +761,9 @@ public Collection getRepairSchedulesForCluster(String clusterNam public Collection getRepairSchedulesForKeyspace(String keyspaceName) { Collection schedules = Lists.newArrayList(); ResultSet scheduleIds = session.execute(getRepairScheduleByClusterAndKsPrepStmt.bind(" ", keyspaceName)); - for(Row scheduleId:scheduleIds){ + for (Row scheduleId : scheduleIds) { Optional schedule = getRepairSchedule(scheduleId.getUUID("repair_schedule_id")); - if(schedule.isPresent()){ + if (schedule.isPresent()) { schedules.add(schedule.get()); } } @@ -679,9 +775,9 @@ public Collection getRepairSchedulesForKeyspace(String keyspaceN public Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, String keyspaceName) { Collection schedules = Lists.newArrayList(); ResultSet scheduleIds = session.execute(getRepairScheduleByClusterAndKsPrepStmt.bind(clusterName, keyspaceName)); - for(Row scheduleId:scheduleIds){ + for (Row scheduleId : scheduleIds) { Optional schedule = getRepairSchedule(scheduleId.getUUID("repair_schedule_id")); - if(schedule.isPresent()){ + if (schedule.isPresent()) { schedules.add(schedule.get()); } } @@ -695,7 +791,7 @@ public Collection getAllRepairSchedules() { Statement stmt = new SimpleStatement(SELECT_REPAIR_SCHEDULE); stmt.setIdempotent(Boolean.TRUE); ResultSet scheduleResults = session.execute(stmt); - for(Row scheduleRow:scheduleResults){ + for (Row scheduleRow : scheduleResults) { schedules.add(createRepairScheduleFromRow(scheduleRow)); } @@ -709,32 +805,41 @@ public boolean updateRepairSchedule(RepairSchedule newRepairSchedule) { RepairUnit repairUnit = getRepairUnit(newRepairSchedule.getRepairUnitId()).get(); List futures = Lists.newArrayList(); - futures.add(session.executeAsync(insertRepairSchedulePrepStmt.bind(newRepairSchedule.getId(), - newRepairSchedule.getRepairUnitId(), - newRepairSchedule.getState().toString(), - newRepairSchedule.getDaysBetween(), - newRepairSchedule.getNextActivation(), - repairHistory, - newRepairSchedule.getSegmentCount(), - newRepairSchedule.getRepairParallelism().toString(), - newRepairSchedule.getIntensity(), - newRepairSchedule.getCreationTime(), - newRepairSchedule.getOwner(), - newRepairSchedule.getPauseTime()))); - - futures.add(session.executeAsync(insertRepairScheduleByClusterAndKsPrepStmt - .bind(repairUnit.getClusterName(), repairUnit.getKeyspaceName(), newRepairSchedule.getId()))); - - futures.add(session.executeAsync(insertRepairScheduleByClusterAndKsPrepStmt - .bind(repairUnit.getClusterName(), " ", newRepairSchedule.getId()))); - - futures.add(session.executeAsync(insertRepairScheduleByClusterAndKsPrepStmt - .bind(" ", repairUnit.getKeyspaceName(), newRepairSchedule.getId()))); + futures.add( + session.executeAsync( + insertRepairSchedulePrepStmt.bind( + newRepairSchedule.getId(), + newRepairSchedule.getRepairUnitId(), + newRepairSchedule.getState().toString(), + newRepairSchedule.getDaysBetween(), + newRepairSchedule.getNextActivation(), + repairHistory, + newRepairSchedule.getSegmentCount(), + newRepairSchedule.getRepairParallelism().toString(), + newRepairSchedule.getIntensity(), + newRepairSchedule.getCreationTime(), + newRepairSchedule.getOwner(), + newRepairSchedule.getPauseTime()))); + + futures.add( + session.executeAsync( + insertRepairScheduleByClusterAndKsPrepStmt.bind( + repairUnit.getClusterName(), repairUnit.getKeyspaceName(), newRepairSchedule.getId()))); + + futures.add( + session.executeAsync( + insertRepairScheduleByClusterAndKsPrepStmt.bind( + repairUnit.getClusterName(), " ", newRepairSchedule.getId()))); + + futures.add( + session.executeAsync( + insertRepairScheduleByClusterAndKsPrepStmt.bind( + " ", repairUnit.getKeyspaceName(), newRepairSchedule.getId()))); try { - Futures.allAsList(futures).get(); + Futures.allAsList(futures).get(); } catch (InterruptedException | ExecutionException ex) { - LOG.error("failed to quorum update repair schedule " + newRepairSchedule.getId(), ex); + LOG.error("failed to quorum update repair schedule " + newRepairSchedule.getId(), ex); } return true; @@ -743,17 +848,20 @@ public boolean updateRepairSchedule(RepairSchedule newRepairSchedule) { @Override public Optional deleteRepairSchedule(UUID id) { Optional repairSchedule = getRepairSchedule(id); - if(repairSchedule.isPresent()){ + if (repairSchedule.isPresent()) { RepairUnit repairUnit = getRepairUnit(repairSchedule.get().getRepairUnitId()).get(); - session.executeAsync(deleteRepairScheduleByClusterAndKsPrepStmt - .bind(repairUnit.getClusterName(), repairUnit.getKeyspaceName(), repairSchedule.get().getId())); + session.executeAsync( + deleteRepairScheduleByClusterAndKsPrepStmt.bind( + repairUnit.getClusterName(), repairUnit.getKeyspaceName(), repairSchedule.get().getId())); - session.executeAsync(deleteRepairScheduleByClusterAndKsPrepStmt - .bind(repairUnit.getClusterName(), " ", repairSchedule.get().getId())); + session.executeAsync( + deleteRepairScheduleByClusterAndKsPrepStmt.bind( + repairUnit.getClusterName(), " ", repairSchedule.get().getId())); - session.executeAsync(deleteRepairScheduleByClusterAndKsPrepStmt - .bind(" ", repairUnit.getKeyspaceName(), repairSchedule.get().getId())); + session.executeAsync( + deleteRepairScheduleByClusterAndKsPrepStmt.bind( + " ", repairUnit.getKeyspaceName(), repairSchedule.get().getId())); session.executeAsync(deleteRepairSchedulePrepStmt.bind(repairSchedule.get().getId())); } @@ -765,13 +873,12 @@ public Optional deleteRepairSchedule(UUID id) { public Collection getClusterRunStatuses(String clusterName, int limit) { Collection repairRunStatuses = Lists.newArrayList(); Collection repairRuns = getRepairRunsForCluster(clusterName); - for (RepairRun repairRun:repairRuns){ + for (RepairRun repairRun : repairRuns) { Collection segments = getRepairSegmentsForRun(repairRun.getId()); Optional repairUnit = getRepairUnit(repairRun.getRepairUnitId()); - int segmentsRepaired = (int) segments.stream() - .filter(seg -> seg.getState().equals(RepairSegment.State.DONE)) - .count(); + int segmentsRepaired + = (int) segments.stream().filter(seg -> seg.getState().equals(RepairSegment.State.DONE)).count(); repairRunStatuses.add(new RepairRunStatus(repairRun, repairUnit.get(), segmentsRepaired)); } @@ -784,24 +891,22 @@ public Collection getClusterScheduleStatuses(String cluste Collection repairSchedules = getRepairSchedulesForCluster(clusterName); Collection repairScheduleStatuses = repairSchedules - .stream() - .map(sched -> new RepairScheduleStatus(sched, getRepairUnit(sched.getRepairUnitId()).get())) - .collect(Collectors.toList()); + .stream() + .map(sched -> new RepairScheduleStatus(sched, getRepairUnit(sched.getRepairUnitId()).get())) + .collect(Collectors.toList()); return repairScheduleStatuses; } - - - private RepairRun buildRepairRunFromRow(Row repairRunResult, UUID id){ + private RepairRun buildRepairRunFromRow(Row repairRunResult, UUID id) { LOG.debug("buildRepairRunFromRow {} / {}", id, repairRunResult); return new RepairRun.Builder( - repairRunResult.getString("cluster_name"), - repairRunResult.getUUID("repair_unit_id"), - new DateTime(repairRunResult.getTimestamp("creation_time")), - repairRunResult.getDouble("intensity"), - repairRunResult.getInt("segment_count"), - RepairParallelism.fromName(repairRunResult.getString("repair_parallelism"))) + repairRunResult.getString("cluster_name"), + repairRunResult.getUUID("repair_unit_id"), + new DateTime(repairRunResult.getTimestamp("creation_time")), + repairRunResult.getDouble("intensity"), + repairRunResult.getInt("segment_count"), + RepairParallelism.fromName(repairRunResult.getString("repair_parallelism"))) .cause(repairRunResult.getString("cause")) .owner(repairRunResult.getString("owner")) .endTime(new DateTime(repairRunResult.getTimestamp("end_time"))) @@ -816,14 +921,14 @@ private RepairRun buildRepairRunFromRow(Row repairRunResult, UUID id){ public boolean takeLead(UUID leaderId) { LOG.debug("Trying to take lead on segment {}", leaderId); ResultSet lwtResult = session.execute( - takeLeadPrepStmt.bind(leaderId, AppContext.REAPER_INSTANCE_ID, AppContext.REAPER_INSTANCE_ADDRESS)); + takeLeadPrepStmt.bind(leaderId, AppContext.REAPER_INSTANCE_ID, AppContext.REAPER_INSTANCE_ADDRESS)); if (lwtResult.wasApplied()) { LOG.debug("Took lead on segment {}", leaderId); return true; } - // Another instance took the lead on the segment + // Another instance took the lead on the segmen LOG.debug("Could not take lead on segment {}", leaderId); return false; } @@ -831,11 +936,11 @@ public boolean takeLead(UUID leaderId) { @Override public boolean renewLead(UUID leaderId) { ResultSet lwtResult = session.execute( - renewLeadPrepStmt.bind( - AppContext.REAPER_INSTANCE_ID, - AppContext.REAPER_INSTANCE_ADDRESS, - leaderId, - AppContext.REAPER_INSTANCE_ID)); + renewLeadPrepStmt.bind( + AppContext.REAPER_INSTANCE_ID, + AppContext.REAPER_INSTANCE_ADDRESS, + leaderId, + AppContext.REAPER_INSTANCE_ID)); if (lwtResult.wasApplied()) { LOG.debug("Renewed lead on segment {}", leaderId); @@ -860,11 +965,11 @@ public void releaseLead(UUID leaderId) { private boolean hasLeadOnSegment(UUID leaderId) { ResultSet lwtResult = session.execute( - renewLeadPrepStmt.bind( - AppContext.REAPER_INSTANCE_ID, - AppContext.REAPER_INSTANCE_ADDRESS, - leaderId, - AppContext.REAPER_INSTANCE_ID)); + renewLeadPrepStmt.bind( + AppContext.REAPER_INSTANCE_ID, + AppContext.REAPER_INSTANCE_ADDRESS, + leaderId, + AppContext.REAPER_INSTANCE_ID)); return lwtResult.wasApplied(); } @@ -872,26 +977,28 @@ private boolean hasLeadOnSegment(UUID leaderId) { @Override public void storeNodeMetrics(NodeMetrics hostMetrics) { session.execute( - storeNodeMetricsPrepStmt.bind( - hostMetrics.getHostAddress(), - hostMetrics.getDatacenter(), - hostMetrics.getPendingCompactions(), - hostMetrics.hasRepairRunning(), - hostMetrics.getActiveAnticompactions())); + storeNodeMetricsPrepStmt.bind( + hostMetrics.getHostAddress(), + hostMetrics.getDatacenter(), + hostMetrics.getPendingCompactions(), + hostMetrics.hasRepairRunning(), + hostMetrics.getActiveAnticompactions())); } @Override public Optional getNodeMetrics(String hostName) { Row metrics = session.execute(getNodeMetricsPrepStmt.bind(hostName)).one(); if (null != metrics) { - return Optional.of(NodeMetrics.builder().withHostAddress(hostName) - .withDatacenter(metrics.getString("datacenter")) - .withPendingCompactions(metrics.getInt("pending_compactions")) - .withHasRepairRunning(metrics.getBool("has_repair_running")) - .withActiveAnticompactions(metrics.getInt("active_anticompactions")) - .build()); - } else { - return Optional.absent(); + return Optional.of( + NodeMetrics.builder() + .withHostAddress(hostName) + .withDatacenter(metrics.getString("datacenter")) + .withPendingCompactions(metrics.getInt("pending_compactions")) + .withHasRepairRunning(metrics.getBool("has_repair_running")) + .withActiveAnticompactions(metrics.getInt("active_anticompactions")) + .build()); + } else { + return Optional.absent(); } } @@ -900,15 +1007,16 @@ public int countRunningReapers() { ResultSet result = session.execute(getRunningReapersCountPrepStmt.bind()); int runningReapers = (int) result.one().getLong("nb_reapers"); LOG.debug("Running reapers = {}", runningReapers); - return runningReapers>0?runningReapers:1; + return runningReapers > 0 ? runningReapers : 1; } @Override public void saveHeartbeat() { DateTime now = DateTime.now(); // Send heartbeats every minute - if(now.minusSeconds(60).getMillis() >= lastHeartBeat.getMillis()) { - session.executeAsync(saveHeartbeatPrepStmt.bind(AppContext.REAPER_INSTANCE_ID, AppContext.REAPER_INSTANCE_ADDRESS)); + if (now.minusSeconds(60).getMillis() >= lastHeartBeat.getMillis()) { + session.executeAsync( + saveHeartbeatPrepStmt.bind(AppContext.REAPER_INSTANCE_ID, AppContext.REAPER_INSTANCE_ADDRESS)); lastHeartBeat = now; } @@ -921,50 +1029,67 @@ private static boolean withinRange(RepairSegment segment, Optional ra /** * Retry all statements. * - * All reaper statements are idempotent. - * Reaper generates few read and writes requests, so it's ok to keep retrying. + *

+ * All reaper statements are idempotent. Reaper generates few read and writes requests, so it's ok to keep + * retrying. * - * Sleep 100 milliseconds in between subsequent read retries. - * Fail after the tenth read retry. + *

+ * Sleep 100 milliseconds in between subsequent read retries. Fail after the tenth read retry. * + *

* Writes keep retrying forever. */ private static class RetryPolicyImpl implements RetryPolicy { @Override - public RetryDecision onReadTimeout(Statement stmt, ConsistencyLevel cl, int required, int received, boolean retrieved, int retry) { - if (retry > 1) { - try { - Thread.sleep(100); - } catch (InterruptedException ex) {} - } - return stmt.isIdempotent() - ? retry < 10 ? RetryDecision.retry(cl) : RetryDecision.rethrow() - : DefaultRetryPolicy.INSTANCE.onReadTimeout(stmt, cl, required, received, retrieved, retry); + public RetryDecision onReadTimeout( + Statement stmt, + ConsistencyLevel cl, + int required, + int received, + boolean retrieved, + int retry) { + + if (retry > 1) { + try { + Thread.sleep(100); + } catch (InterruptedException expected) { } + } + return stmt.isIdempotent() + ? retry < 10 ? RetryDecision.retry(cl) : RetryDecision.rethrow() + : DefaultRetryPolicy.INSTANCE.onReadTimeout(stmt, cl, required, received, retrieved, retry); } @Override - public RetryDecision onWriteTimeout(Statement stmt, ConsistencyLevel cl, WriteType type , int required, int received, int retry) { - return stmt.isIdempotent() - ? RetryDecision.retry(cl) - : DefaultRetryPolicy.INSTANCE.onWriteTimeout(stmt, cl, type, required, received, retry); + public RetryDecision onWriteTimeout( + Statement stmt, + ConsistencyLevel cl, + WriteType type, + int required, + int received, + int retry) { + + return stmt.isIdempotent() + ? RetryDecision.retry(cl) + : DefaultRetryPolicy.INSTANCE.onWriteTimeout(stmt, cl, type, required, received, retry); } @Override public RetryDecision onUnavailable(Statement stmt, ConsistencyLevel cl, int required, int aliveReplica, int retry) { - return DefaultRetryPolicy.INSTANCE - .onUnavailable(stmt, cl, required, aliveReplica, retry == 1 ? 0 : retry); + return DefaultRetryPolicy.INSTANCE.onUnavailable(stmt, cl, required, aliveReplica, retry == 1 ? 0 : retry); } @Override - public RetryDecision onRequestError(Statement stmt, ConsistencyLevel cl, DriverException e, int nbRetry) { - return DefaultRetryPolicy.INSTANCE.onRequestError(stmt, cl, e, nbRetry); + public RetryDecision onRequestError(Statement stmt, ConsistencyLevel cl, DriverException ex, int nbRetry) { + return DefaultRetryPolicy.INSTANCE.onRequestError(stmt, cl, ex, nbRetry); } @Override - public void init(com.datastax.driver.core.Cluster cluster) {} + public void init(com.datastax.driver.core.Cluster cluster) { + } @Override - public void close() {} + public void close() { + } } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/IDistributedStorage.java b/src/server/src/main/java/com/spotify/reaper/storage/IDistributedStorage.java index e2bbebd88..aed5ef222 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/IDistributedStorage.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/IDistributedStorage.java @@ -11,9 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage; -import com.google.common.base.Optional; import com.spotify.reaper.core.NodeMetrics; import com.spotify.reaper.core.RepairSegment; import com.spotify.reaper.service.RingRange; @@ -22,6 +22,7 @@ import java.util.List; import java.util.UUID; +import com.google.common.base.Optional; /** * Definition for a storage that can run in distributed (peer-to-peer) mode. For example Cassandra. @@ -29,13 +30,18 @@ public interface IDistributedStorage { boolean takeLead(UUID leaderId); + boolean renewLead(UUID leaderId); + void releaseLead(UUID leaderId); Collection getRepairSegmentsForRunInLocalMode(UUID runId, List localRanges); int countRunningReapers(); + void saveHeartbeat(); + Optional getNodeMetrics(String hostName); + void storeNodeMetrics(NodeMetrics hostMetrics); } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/IStorage.java b/src/server/src/main/java/com/spotify/reaper/storage/IStorage.java index 37d8a00e8..1d6c092ab 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/IStorage.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/IStorage.java @@ -11,12 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper.storage; -import com.google.common.base.Optional; +package com.spotify.reaper.storage; import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.NodeMetrics; import com.spotify.reaper.core.RepairRun; import com.spotify.reaper.core.RepairSchedule; import com.spotify.reaper.core.RepairSegment; @@ -27,15 +25,16 @@ import com.spotify.reaper.service.RingRange; import java.util.Collection; -import java.util.List; import java.util.Set; import java.util.UUID; +import com.google.common.base.Optional; + /** * API definition for cassandra-reaper. */ public interface IStorage { - + boolean isStorageConnected(); Collection getClusters(); @@ -47,8 +46,8 @@ public interface IStorage { Optional getCluster(String clusterName); /** - * Delete the Cluster instance identified by the given cluster name. Delete succeeds - * only if there are no repair runs for the targeted cluster. + * Delete the Cluster instance identified by the given cluster name. Delete succeeds only if there are no repair runs + * for the targeted cluster. * * @param clusterName The name of the Cluster instance to delete. * @return The deleted Cluster instance if delete succeeds, with state set to DELETED. @@ -68,8 +67,7 @@ public interface IStorage { Collection getRepairRunsWithState(RepairRun.RunState runState); /** - * Delete the RepairRun instance identified by the given id, and delete also - * all the related repair segments. + * Delete the RepairRun instance identified by the given id, and delete also all the related repair segments. * * @param id The id of the RepairRun instance to delete, and all segments for it. * @return The deleted RepairRun instance, if delete succeeds, with state set to DELETED. @@ -83,14 +81,12 @@ public interface IStorage { /** * Get a stored RepairUnit targeting the given tables in the given keyspace. * - * @param cluster Cluster name for the RepairUnit. - * @param keyspace Keyspace name for the RepairUnit. + * @param cluster Cluster name for the RepairUnit. + * @param keyspace Keyspace name for the RepairUnit. * @param columnFamilyNames Set of column families targeted by the RepairUnit. * @return Instance of a RepairUnit matching the parameters, or null if not found. */ - Optional getRepairUnit(String cluster, String keyspace, - Set columnFamilyNames); - + Optional getRepairUnit(String cluster, String keyspace, Set columnFamilyNames); boolean updateRepairSegment(RepairSegment newRepairSegment); @@ -100,9 +96,8 @@ Optional getRepairUnit(String cluster, String keyspace, /** * @param runId the run id that the segment belongs to. - * @param range a ring range. The start of the range may be greater than or equal to the end. - * This case has to be handled. When start = end, consider that as a range - * that covers the whole ring. + * @param range a ring range. The start of the range may be greater than or equal to the end. This case has to be + * handled. When start = end, consider that as a range that covers the whole ring. * @return a segment enclosed by the range with state NOT_STARTED, or nothing. */ Optional getNextFreeSegmentInRange(UUID runId, Optional range); @@ -125,16 +120,15 @@ Optional getRepairUnit(String cluster, String keyspace, Collection getRepairSchedulesForKeyspace(String keyspaceName); - Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, - String keyspaceName); + Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, String keyspaceName); Collection getAllRepairSchedules(); boolean updateRepairSchedule(RepairSchedule newRepairSchedule); /** - * Delete the RepairSchedule instance identified by the given id. Related repair runs - * or other resources tied to the schedule will not be deleted. + * Delete the RepairSchedule instance identified by the given id. Related repair runs or other resources tied to the + * schedule will not be deleted. * * @param id The id of the RepairSchedule instance to delete. * @return The deleted RepairSchedule instance, if delete succeeds, with state set to DELETED. @@ -144,5 +138,4 @@ Collection getRepairSchedulesForClusterAndKeyspace(String cluste Collection getClusterRunStatuses(String clusterName, int limit); Collection getClusterScheduleStatuses(String clusterName); - } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/MemoryStorage.java b/src/server/src/main/java/com/spotify/reaper/storage/MemoryStorage.java index 6f858c044..36d4ab5cc 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/MemoryStorage.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/MemoryStorage.java @@ -11,8 +11,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.view.RepairRunStatus; +import com.spotify.reaper.resources.view.RepairScheduleStatus; +import com.spotify.reaper.service.RepairParameters; +import com.spotify.reaper.service.RingRange; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,15 +40,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.view.RepairRunStatus; -import com.spotify.reaper.resources.view.RepairScheduleStatus; -import com.spotify.reaper.service.RepairParameters; -import com.spotify.reaper.service.RingRange; /** * Implements the StorageAPI using transient Java classes. @@ -49,8 +51,7 @@ public final class MemoryStorage implements IStorage { private final ConcurrentMap repairUnits = Maps.newConcurrentMap(); private final ConcurrentMap repairUnitsByKey = Maps.newConcurrentMap(); private final ConcurrentMap repairSegments = Maps.newConcurrentMap(); - private final ConcurrentMap> repairSegmentsByRunId = - Maps.newConcurrentMap(); + private final ConcurrentMap> repairSegmentsByRunId = Maps.newConcurrentMap(); private final ConcurrentMap repairSchedules = Maps.newConcurrentMap(); @Override @@ -87,8 +88,7 @@ public Optional getCluster(String clusterName) { @Override public Optional deleteCluster(String clusterName) { - if (getRepairSchedulesForCluster(clusterName).isEmpty() - && getRepairRunsForCluster(clusterName).isEmpty()) { + if (getRepairSchedulesForCluster(clusterName).isEmpty() && getRepairRunsForCluster(clusterName).isEmpty()) { return Optional.fromNullable(clusters.remove(clusterName)); } return Optional.absent(); @@ -205,8 +205,8 @@ public Optional deleteRepairRun(UUID id) { @Override public RepairUnit addRepairUnit(RepairUnit.Builder repairUnit) { - Optional existing = - getRepairUnit(repairUnit.clusterName, repairUnit.keyspaceName, repairUnit.columnFamilies); + Optional existing + = getRepairUnit(repairUnit.clusterName, repairUnit.keyspaceName, repairUnit.columnFamilies); if (existing.isPresent() && repairUnit.incrementalRepair == existing.get().getIncrementalRepair()) { return existing.get(); } else { @@ -225,8 +225,7 @@ public Optional getRepairUnit(UUID id) { @Override public Optional getRepairUnit(String cluster, String keyspace, Set tables) { - return Optional.fromNullable( - repairUnitsByKey.get(new RepairUnitKey(cluster, keyspace, tables))); + return Optional.fromNullable(repairUnitsByKey.get(new RepairUnitKey(cluster, keyspace, tables))); } private void addRepairSegments(Collection segments, UUID runId) { @@ -245,8 +244,7 @@ public boolean updateRepairSegment(RepairSegment newRepairSegment) { return false; } else { repairSegments.put(newRepairSegment.getId(), newRepairSegment); - LinkedHashMap updatedSegment = - repairSegmentsByRunId.get(newRepairSegment.getRunId()); + LinkedHashMap updatedSegment = repairSegmentsByRunId.get(newRepairSegment.getRunId()); updatedSegment.put(newRepairSegment.getId(), newRepairSegment); return true; } @@ -274,13 +272,13 @@ private Optional getNextFreeSegment(UUID runId) { @Override public Optional getNextFreeSegmentInRange(UUID runId, Optional range) { if (range.isPresent()) { - for (RepairSegment segment : repairSegmentsByRunId.get(runId).values()) { - if (segment.getState() == RepairSegment.State.NOT_STARTED && range.get().encloses(segment.getTokenRange())) { - return Optional.of(segment); - } + for (RepairSegment segment : repairSegmentsByRunId.get(runId).values()) { + if (segment.getState() == RepairSegment.State.NOT_STARTED && range.get().encloses(segment.getTokenRange())) { + return Optional.of(segment); } + } } else { - return getNextFreeSegment(runId); + return getNextFreeSegment(runId); } return Optional.absent(); } @@ -302,11 +300,9 @@ public Collection getOngoingRepairsInCluster(String clusterNam for (RepairRun run : getRepairRunsWithState(RepairRun.RunState.RUNNING)) { for (RepairSegment segment : getSegmentsWithState(run.getId(), RepairSegment.State.RUNNING)) { RepairUnit unit = getRepairUnit(segment.getRepairUnitId()).get(); - ongoingRepairs.add(new RepairParameters( - segment.getTokenRange(), - unit.getKeyspaceName(), - unit.getColumnFamilies(), - run.getRepairParallelism())); + ongoingRepairs.add( + new RepairParameters( + segment.getTokenRange(), unit.getKeyspaceName(), unit.getColumnFamilies(), run.getRepairParallelism())); } } return ongoingRepairs; @@ -343,7 +339,6 @@ public int getSegmentAmountForRepairRunWithState(UUID runId, RepairSegment.State return amount; } - @Override public RepairSchedule addRepairSchedule(RepairSchedule.Builder repairSchedule) { RepairSchedule newRepairSchedule = repairSchedule.build(UUIDs.timeBased()); @@ -381,13 +376,11 @@ public Collection getRepairSchedulesForKeyspace(String keyspaceN } @Override - public Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, - String keyspaceName) { + public Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, String keyspaceName) { Collection foundRepairSchedules = new ArrayList<>(); for (RepairSchedule repairSchedule : repairSchedules.values()) { RepairUnit repairUnit = getRepairUnit(repairSchedule.getRepairUnitId()).get(); - if (repairUnit.getClusterName().equals(clusterName) && repairUnit.getKeyspaceName() - .equals(keyspaceName)) { + if (repairUnit.getClusterName().equals(clusterName) && repairUnit.getKeyspaceName().equals(keyspaceName)) { foundRepairSchedules.add(repairSchedule); } } @@ -429,15 +422,29 @@ public Collection getClusterRunStatuses(String clusterName, int Collections.sort(runs); for (RepairRun run : Iterables.limit(runs, limit)) { RepairUnit unit = getRepairUnit(run.getRepairUnitId()).get(); - int segmentsRepaired = - getSegmentAmountForRepairRunWithState(run.getId(), RepairSegment.State.DONE); + int segmentsRepaired = getSegmentAmountForRepairRunWithState(run.getId(), RepairSegment.State.DONE); int totalSegments = getSegmentAmountForRepairRun(run.getId()); - runStatuses.add(new RepairRunStatus( - run.getId(), clusterName, unit.getKeyspaceName(), unit.getColumnFamilies(), - segmentsRepaired, totalSegments, run.getRunState(), run.getStartTime(), - run.getEndTime(), run.getCause(), run.getOwner(), run.getLastEvent(), - run.getCreationTime(), run.getPauseTime(), run.getIntensity(), unit.getIncrementalRepair(), - run.getRepairParallelism(), unit.getNodes(), unit.getDatacenters())); + runStatuses.add( + new RepairRunStatus( + run.getId(), + clusterName, + unit.getKeyspaceName(), + unit.getColumnFamilies(), + segmentsRepaired, + totalSegments, + run.getRunState(), + run.getStartTime(), + run.getEndTime(), + run.getCause(), + run.getOwner(), + run.getLastEvent(), + run.getCreationTime(), + run.getPauseTime(), + run.getIntensity(), + unit.getIncrementalRepair(), + run.getRepairParallelism(), + unit.getNodes(), + unit.getDatacenters())); } return runStatuses; } @@ -477,10 +484,10 @@ public RepairUnitKey(String cluster, String keyspace, Set tables) { @Override public boolean equals(Object other) { - return other instanceof RepairUnitKey && - cluster.equals(((RepairUnitKey) other).cluster) && - keyspace.equals(((RepairUnitKey) other).keyspace) && - tables.equals(((RepairUnitKey) other).tables); + return other instanceof RepairUnitKey + && cluster.equals(((RepairUnitKey) other).cluster) + && keyspace.equals(((RepairUnitKey) other).keyspace) + && tables.equals(((RepairUnitKey) other).tables); } @Override @@ -488,5 +495,4 @@ public int hashCode() { return cluster.hashCode() ^ keyspace.hashCode() ^ tables.hashCode(); } } - } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/PostgresStorage.java b/src/server/src/main/java/com/spotify/reaper/storage/PostgresStorage.java index 7de283960..7dcca99e1 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/PostgresStorage.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/PostgresStorage.java @@ -11,10 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; import com.spotify.reaper.core.Cluster; import com.spotify.reaper.core.RepairRun; @@ -25,13 +24,16 @@ import com.spotify.reaper.resources.view.RepairScheduleStatus; import com.spotify.reaper.service.RepairParameters; import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.storage.postgresql.*; - -import org.skife.jdbi.v2.DBI; -import org.skife.jdbi.v2.Handle; -import org.skife.jdbi.v2.exceptions.DBIException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.spotify.reaper.storage.postgresql.BigIntegerArgumentFactory; +import com.spotify.reaper.storage.postgresql.IStoragePostgreSql; +import com.spotify.reaper.storage.postgresql.LongCollectionSqlTypeArgumentFactory; +import com.spotify.reaper.storage.postgresql.PostgresArrayArgumentFactory; +import com.spotify.reaper.storage.postgresql.RepairParallelismArgumentFactory; +import com.spotify.reaper.storage.postgresql.RunStateArgumentFactory; +import com.spotify.reaper.storage.postgresql.ScheduleStateArgumentFactory; +import com.spotify.reaper.storage.postgresql.StateArgumentFactory; +import com.spotify.reaper.storage.postgresql.UuidArgumentFactory; +import com.spotify.reaper.storage.postgresql.UuidUtil; import java.util.ArrayList; import java.util.Collection; @@ -39,6 +41,13 @@ import java.util.Set; import java.util.UUID; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.exceptions.DBIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Implements the StorageAPI using PostgreSQL database. @@ -53,16 +62,16 @@ public PostgresStorage(DBI jdbi) { this.jdbi = jdbi; } - private static IStoragePostgreSQL getPostgresStorage(Handle h) { - h.registerArgumentFactory(new LongCollectionSQLTypeArgumentFactory()); - h.registerArgumentFactory(new PostgresArrayArgumentFactory()); - h.registerArgumentFactory(new RunStateArgumentFactory()); - h.registerArgumentFactory(new RepairParallelismArgumentFactory()); - h.registerArgumentFactory(new StateArgumentFactory()); - h.registerArgumentFactory(new BigIntegerArgumentFactory()); - h.registerArgumentFactory(new ScheduleStateArgumentFactory()); - h.registerArgumentFactory(new UuidArgumentFactory()); - return h.attach(IStoragePostgreSQL.class); + private static IStoragePostgreSql getPostgresStorage(Handle handle) { + handle.registerArgumentFactory(new LongCollectionSqlTypeArgumentFactory()); + handle.registerArgumentFactory(new PostgresArrayArgumentFactory()); + handle.registerArgumentFactory(new RunStateArgumentFactory()); + handle.registerArgumentFactory(new RepairParallelismArgumentFactory()); + handle.registerArgumentFactory(new StateArgumentFactory()); + handle.registerArgumentFactory(new BigIntegerArgumentFactory()); + handle.registerArgumentFactory(new ScheduleStateArgumentFactory()); + handle.registerArgumentFactory(new UuidArgumentFactory()); + return handle.attach(IStoragePostgreSql.class); } @Override @@ -78,7 +87,7 @@ public Optional getCluster(String clusterName) { public Optional deleteCluster(String clusterName) { Cluster result = null; try (Handle h = jdbi.open()) { - IStoragePostgreSQL pg = getPostgresStorage(h); + IStoragePostgreSql pg = getPostgresStorage(h); Cluster clusterToDel = pg.getCluster(clusterName); if (clusterToDel != null) { int rowsDeleted = pg.deleteCluster(clusterName); @@ -95,7 +104,7 @@ public boolean isStorageConnected() { String currentDate = null; if (null != jdbi) { try (Handle h = jdbi.open()) { - currentDate = getPostgresStorage(h).getCurrentDate(); + currentDate = getPostgresStorage(h).getCurrentDate(); } } return null != currentDate && !currentDate.trim().isEmpty(); @@ -177,34 +186,33 @@ public Collection getRepairRunsWithState(RepairRun.RunState runState) @Override public Optional deleteRepairRun(UUID id) { RepairRun result = null; - Handle h = null; + Handle handle = null; try { - h = jdbi.open(); - h.begin(); - IStoragePostgreSQL pg = getPostgresStorage(h); + handle = jdbi.open(); + handle.begin(); + IStoragePostgreSql pg = getPostgresStorage(handle); RepairRun runToDelete = pg.getRepairRun(UuidUtil.toSequenceId(id)); if (runToDelete != null) { - int segmentsRunning = pg.getSegmentAmountForRepairRunWithState(UuidUtil.toSequenceId(id), - RepairSegment.State.RUNNING); + int segmentsRunning + = pg.getSegmentAmountForRepairRunWithState(UuidUtil.toSequenceId(id), RepairSegment.State.RUNNING); if (segmentsRunning == 0) { pg.deleteRepairSegmentsForRun(UuidUtil.toSequenceId(runToDelete.getId())); pg.deleteRepairRun(UuidUtil.toSequenceId(id)); result = runToDelete.with().runState(RepairRun.RunState.DELETED).build(id); } else { - LOG.warn("not deleting RepairRun \"{}\" as it has segments running: {}", - id, segmentsRunning); + LOG.warn("not deleting RepairRun \"{}\" as it has segments running: {}", id, segmentsRunning); } } - h.commit(); + handle.commit(); } catch (DBIException ex) { LOG.warn("DELETE failed", ex); ex.printStackTrace(); - if (h != null) { - h.rollback(); + if (handle != null) { + handle.rollback(); } } finally { - if (h != null) { - h.close(); + if (handle != null) { + handle.close(); } } if (result != null) { @@ -215,7 +223,7 @@ public Optional deleteRepairRun(UUID id) { private void tryDeletingRepairUnit(UUID id) { try (Handle h = jdbi.open()) { - IStoragePostgreSQL pg = getPostgresStorage(h); + IStoragePostgreSql pg = getPostgresStorage(h); pg.deleteRepairUnit(UuidUtil.toSequenceId(id)); } catch (DBIException ex) { LOG.info("cannot delete RepairUnit with id " + id); @@ -266,11 +274,10 @@ public Optional getRepairUnit(UUID id) { } @Override - public Optional getRepairUnit(String clusterName, String keyspaceName, - Set columnFamilies) { + public Optional getRepairUnit(String clusterName, String keyspaceName, Set columnFamilies) { RepairUnit result; try (Handle h = jdbi.open()) { - IStoragePostgreSQL storage = getPostgresStorage(h); + IStoragePostgreSql storage = getPostgresStorage(h); result = storage.getRepairUnitByClusterAndTables(clusterName, keyspaceName, columnFamilies); } return Optional.fromNullable(result); @@ -327,20 +334,20 @@ private Optional getNextFreeSegment(UUID runId) { @Override public Optional getNextFreeSegmentInRange(UUID runId, Optional range) { if (range.isPresent()) { - RepairSegment result; - try (Handle h = jdbi.open()) { - IStoragePostgreSQL storage = getPostgresStorage(h); - if (!range.get().isWrapping()) { - result = storage.getNextFreeRepairSegmentInNonWrappingRange(UuidUtil.toSequenceId(runId), range.get().getStart(), - range.get().getEnd()); - } else { - result = storage.getNextFreeRepairSegmentInWrappingRange(UuidUtil.toSequenceId(runId), range.get().getStart(), - range.get().getEnd()); - } + RepairSegment result; + try (Handle h = jdbi.open()) { + IStoragePostgreSql storage = getPostgresStorage(h); + if (!range.get().isWrapping()) { + result = storage.getNextFreeRepairSegmentInNonWrappingRange( + UuidUtil.toSequenceId(runId), range.get().getStart(), range.get().getEnd()); + } else { + result = storage.getNextFreeRepairSegmentInWrappingRange( + UuidUtil.toSequenceId(runId), range.get().getStart(), range.get().getEnd()); } - return Optional.fromNullable(result); + } + return Optional.fromNullable(result); } else { - return getNextFreeSegment(runId); + return getNextFreeSegment(runId); } } @@ -364,8 +371,8 @@ public Collection getOngoingRepairsInCluster(String clusterNam public Collection getRepairRunIdsForCluster(String clusterName) { Collection result = Lists.newArrayList(); try (Handle h = jdbi.open()) { - for(Long l: getPostgresStorage(h).getRepairRunIdsForCluster(clusterName)){ - result.add(UuidUtil.fromSequenceId(l)); + for (Long l : getPostgresStorage(h).getRepairRunIdsForCluster(clusterName)) { + result.add(UuidUtil.fromSequenceId(l)); } } return result; @@ -424,12 +431,10 @@ public Collection getRepairSchedulesForKeyspace(String keyspaceN } @Override - public Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, - String keyspaceName) { + public Collection getRepairSchedulesForClusterAndKeyspace(String clusterName, String keyspaceName) { Collection result; try (Handle h = jdbi.open()) { - result = - getPostgresStorage(h).getRepairSchedulesForClusterAndKeySpace(clusterName, keyspaceName); + result = getPostgresStorage(h).getRepairSchedulesForClusterAndKeySpace(clusterName, keyspaceName); } return result; } @@ -461,7 +466,7 @@ public boolean updateRepairSchedule(RepairSchedule newRepairSchedule) { public Optional deleteRepairSchedule(UUID id) { RepairSchedule result = null; try (Handle h = jdbi.open()) { - IStoragePostgreSQL pg = getPostgresStorage(h); + IStoragePostgreSql pg = getPostgresStorage(h); RepairSchedule scheduleToDel = pg.getRepairSchedule(UuidUtil.toSequenceId(id)); if (scheduleToDel != null) { int rowsDeleted = pg.deleteRepairSchedule(UuidUtil.toSequenceId(scheduleToDel.getId())); @@ -489,5 +494,4 @@ public Collection getClusterScheduleStatuses(String cluste return getPostgresStorage(h).getClusterScheduleOverview(clusterName); } } - } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/cassandra/DateTimeCodec.java b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/DateTimeCodec.java index c1d6811c1..5f25ee32d 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/cassandra/DateTimeCodec.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/DateTimeCodec.java @@ -1,23 +1,39 @@ +/* + * 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.spotify.reaper.storage.cassandra; + +import java.nio.ByteBuffer; + import com.datastax.driver.core.DataType; import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.TypeCodec; import com.datastax.driver.core.exceptions.InvalidTypeException; import org.joda.time.DateTime; -import java.nio.ByteBuffer; - +public final class DateTimeCodec extends TypeCodec { -public class DateTimeCodec extends TypeCodec { public DateTimeCodec() { super(DataType.timestamp(), DateTime.class); } @Override public DateTime parse(String value) { - if (value == null || value.equals("NULL")) + if (value == null || value.equals("NULL")) { return null; + } try { return DateTime.parse(value); @@ -28,73 +44,78 @@ public DateTime parse(String value) { @Override public String format(DateTime value) { - if (value == null) + if (value == null) { return "NULL"; + } return Long.toString(value.getMillis()); } @Override public ByteBuffer serialize(DateTime value, ProtocolVersion protocolVersion) { - return value == null ? null : BigintCodec.instance.serializeNoBoxing(value.getMillis(), protocolVersion); + return value == null ? null : BigintCodec.INSTANCE.serializeNoBoxing(value.getMillis(), protocolVersion); } @Override public DateTime deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 ? null: new DateTime(BigintCodec.instance.deserializeNoBoxing(bytes, protocolVersion)); + return bytes == null || bytes.remaining() == 0 + ? null + : new DateTime(BigintCodec.INSTANCE.deserializeNoBoxing(bytes, protocolVersion)); } - + /** - * Base class for codecs handling CQL 8-byte integer types such as {@link DataType#bigint()}, - * {@link DataType#counter()} or {@link DataType#time()}. + * Base class for codecs handling CQL 8-byte integer types such as {@link DataType#bigint()}, {@link + * DataType#counter()} or {@link DataType#time()}. */ private abstract static class LongCodec extends PrimitiveLongCodec { - private LongCodec(DataType cqlType) { - super(cqlType); - } - - @Override - public Long parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") ? null : Long.parseLong(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException(String.format("Cannot parse 64-bits long value from \"%s\"", value)); - } - } + private LongCodec(DataType cqlType) { + super(cqlType); + } - @Override - public String format(Long value) { - if (value == null) - return "NULL"; - return Long.toString(value); + @Override + public Long parse(String value) { + try { + return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") ? null : Long.parseLong(value); + } catch (NumberFormatException e) { + throw new InvalidTypeException(String.format("Cannot parse 64-bits long value from \"%s\"", value)); } + } - @Override - public ByteBuffer serializeNoBoxing(long value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(8); - bb.putLong(0, value); - return bb; + @Override + public String format(Long value) { + if (value == null) { + return "NULL"; } + return Long.toString(value); + } - @Override - public long deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) - return 0; - if (bytes.remaining() != 8) - throw new InvalidTypeException("Invalid 64-bits long value, expecting 8 bytes but got " + bytes.remaining()); + @Override + public ByteBuffer serializeNoBoxing(long value, ProtocolVersion protocolVersion) { + ByteBuffer bb = ByteBuffer.allocate(8); + bb.putLong(0, value); + return bb; + } - return bytes.getLong(bytes.position()); + @Override + public long deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; } + if (bytes.remaining() != 8) { + throw new InvalidTypeException("Invalid 64-bits long value, expecting 8 bytes but got " + bytes.remaining()); + } + + return bytes.getLong(bytes.position()); + } } - - private static class BigintCodec extends LongCodec { - private static final BigintCodec instance = new BigintCodec(); + private static final class BigintCodec extends LongCodec { - private BigintCodec() { - super(DataType.bigint()); - } + private static final BigintCodec INSTANCE = new BigintCodec(); + private BigintCodec() { + super(DataType.bigint()); + } } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/cassandra/Migration003.java b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/Migration003.java index 44972313f..769356ee1 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/cassandra/Migration003.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/Migration003.java @@ -1,91 +1,111 @@ +/* + * 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.spotify.reaper.storage.cassandra; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.utils.UUIDs; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class Migration003 { - private static final Logger LOG = LoggerFactory.getLogger(Migration003.class); - - /** migrate over the repair_schedule table **/ - public static void migrate(Session session) { - KeyspaceMetadata metadata = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace()); - if(null != metadata.getTable("repair_unit")) { - - LOG.warn("Migrating repair_unit and repair_schedule tables. This may take some minutes…"); - - PreparedStatement insertRprUnit = session.prepare( - "INSERT INTO repair_unit_v1 (id, cluster_name, keyspace_name, column_families, incremental_repair) VALUES(?, ?, ?, ?, ?)"); - - PreparedStatement insertRprSched = session.prepare( - "INSERT INTO repair_schedule_v1 (id, repair_unit_id, state, days_between, next_activation, run_history, segment_count, repair_parallelism, intensity, creation_time, owner, pause_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - - PreparedStatement insertRprSchedIdx = session.prepare( - "INSERT INTO repair_schedule_by_cluster_and_keyspace(cluster_name, keyspace_name, repair_schedule_id) VALUES(?, ?, ?)"); - - Map repairUnitIds = new HashMap<>(); - Map repairUnitClusters = new HashMap<>(); - Map repairUnitKeyspaces = new HashMap<>(); - - for(Row row : session.execute(QueryBuilder.select().from("repair_unit"))) { - UUID uuid = UUIDs.timeBased(); - repairUnitIds.put(row.getLong("id"), uuid); - repairUnitClusters.put(row.getLong("id"), row.getString("cluster_name")); - repairUnitKeyspaces.put(row.getLong("id"), row.getString("keyspace_name")); - - session.execute( - insertRprUnit.bind( - uuid, - row.getString("cluster_name"), - row.getString("keyspace_name"), - row.getSet("column_families", String.class), - row.getBool("incremental_repair"))); - } - session.executeAsync("DROP TABLE repair_unit"); - - for(Row row : session.execute(QueryBuilder.select().from("repair_schedule"))) { - UUID uuid = UUIDs.timeBased(); - long repairUnitId = row.getLong("repair_unit_id"); - - session.execute( - insertRprSched.bind( - uuid, - repairUnitIds.get(repairUnitId), - row.getString("state"), - row.getInt("days_between"), - row.getTimestamp("next_activation"), - Collections.emptySet(), - row.getInt("segment_count"), - row.getString("repair_parallelism"), - row.getDouble("intensity"), - row.getTimestamp("creation_time"), - row.getString("owner"), - row.getTimestamp("pause_time"))); - - - session.executeAsync(insertRprSchedIdx - .bind(repairUnitClusters.get(repairUnitId), repairUnitKeyspaces.get(repairUnitId), uuid)); - - session.executeAsync(insertRprSchedIdx.bind(repairUnitClusters.get(repairUnitId), " ", uuid)); - session.executeAsync(insertRprSchedIdx.bind(" ", repairUnitKeyspaces.get(repairUnitId), uuid)); - } - - session.executeAsync("DROP TABLE repair_schedule"); - - LOG.warn("Migration of repair_unit and repair_schedule tables completed."); - } + private static final Logger LOG = LoggerFactory.getLogger(Migration003.class); + + private Migration003() { + } + + /** + * migrate over the repair_schedule table * + */ + public static void migrate(Session session) { + KeyspaceMetadata metadata = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace()); + if (null != metadata.getTable("repair_unit")) { + + LOG.warn("Migrating repair_unit and repair_schedule tables. This may take some minutes…"); + + PreparedStatement insertRprUnit = session.prepare( + "INSERT INTO repair_unit_v1 (id, cluster_name, keyspace_name, column_families, incremental_repair) " + + "VALUES(?, ?, ?, ?, ?)"); + + PreparedStatement insertRprSched = session.prepare( + "INSERT INTO repair_schedule_v1 (id, repair_unit_id, state, days_between, next_activation, run_history, " + + "segment_count, repair_parallelism, intensity, creation_time, owner, pause_time) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + PreparedStatement insertRprSchedIdx = session.prepare( + "INSERT INTO repair_schedule_by_cluster_and_keyspace(cluster_name, keyspace_name, repair_schedule_id) " + + "VALUES(?, ?, ?)"); + + Map repairUnitIds = new HashMap<>(); + Map repairUnitClusters = new HashMap<>(); + Map repairUnitKeyspaces = new HashMap<>(); + + for (Row row : session.execute(QueryBuilder.select().from("repair_unit"))) { + UUID uuid = UUIDs.timeBased(); + repairUnitIds.put(row.getLong("id"), uuid); + repairUnitClusters.put(row.getLong("id"), row.getString("cluster_name")); + repairUnitKeyspaces.put(row.getLong("id"), row.getString("keyspace_name")); + + session.execute( + insertRprUnit.bind( + uuid, + row.getString("cluster_name"), + row.getString("keyspace_name"), + row.getSet("column_families", String.class), + row.getBool("incremental_repair"))); + } + session.executeAsync("DROP TABLE repair_unit"); + + for (Row row : session.execute(QueryBuilder.select().from("repair_schedule"))) { + UUID uuid = UUIDs.timeBased(); + long repairUnitId = row.getLong("repair_unit_id"); + + session.execute( + insertRprSched.bind( + uuid, + repairUnitIds.get(repairUnitId), + row.getString("state"), + row.getInt("days_between"), + row.getTimestamp("next_activation"), + Collections.emptySet(), + row.getInt("segment_count"), + row.getString("repair_parallelism"), + row.getDouble("intensity"), + row.getTimestamp("creation_time"), + row.getString("owner"), + row.getTimestamp("pause_time"))); + + session.executeAsync( + insertRprSchedIdx.bind(repairUnitClusters.get(repairUnitId), repairUnitKeyspaces.get(repairUnitId), uuid)); + + session.executeAsync(insertRprSchedIdx.bind(repairUnitClusters.get(repairUnitId), " ", uuid)); + session.executeAsync(insertRprSchedIdx.bind(" ", repairUnitKeyspaces.get(repairUnitId), uuid)); + } + + session.executeAsync("DROP TABLE repair_schedule"); + + LOG.warn("Migration of repair_unit and repair_schedule tables completed."); } - - private Migration003(){} + } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/cassandra/package-info.java b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/package-info.java new file mode 100644 index 000000000..f5d39aae1 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/storage/cassandra/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.storage.cassandra; diff --git a/src/server/src/main/java/com/spotify/reaper/storage/package-info.java b/src/server/src/main/java/com/spotify/reaper/storage/package-info.java new file mode 100644 index 000000000..e7d610c8a --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/storage/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.storage; diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/BigIntegerArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/BigIntegerArgumentFactory.java index 1330d3b4f..51300137d 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/BigIntegerArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/BigIntegerArgumentFactory.java @@ -1,20 +1,34 @@ +/* + * 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.spotify.reaper.storage.postgresql; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.PreparedStatement; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.sql.PreparedStatement; -import java.sql.SQLException; - /** * Provides JDBI a method to map BigInteger value to a BIGINT value in database. */ -public class BigIntegerArgumentFactory implements ArgumentFactory { +public final class BigIntegerArgumentFactory implements ArgumentFactory { private static final Logger LOG = LoggerFactory.getLogger(BigIntegerArgumentFactory.class); @@ -25,11 +39,8 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx @Override public Argument build(Class expectedType, final BigInteger value, StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - statement.setBigDecimal(position, new BigDecimal(value)); - } + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + statement.setBigDecimal(position, new BigDecimal(value)); }; } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ClusterMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ClusterMapper.java index 3f247ac99..b2b7eba41 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ClusterMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ClusterMapper.java @@ -11,30 +11,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; +import com.spotify.reaper.core.Cluster; + import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; +import com.google.common.collect.Sets; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import com.google.common.collect.Sets; -import com.spotify.reaper.core.Cluster; -public class ClusterMapper implements ResultSetMapper { +public final class ClusterMapper implements ResultSetMapper { - public Cluster map(int index, ResultSet r, StatementContext ctx) throws SQLException { + @Override + public Cluster map(int index, ResultSet rs, StatementContext ctx) throws SQLException { String[] seedHosts = null; - Object obj = r.getArray("seed_hosts").getArray(); - if(obj instanceof String[]) { - seedHosts = (String[])obj; - } else if(obj instanceof Object[]) { - Object[] ol = (Object[])obj; + Object obj = rs.getArray("seed_hosts").getArray(); + if (obj instanceof String[]) { + seedHosts = (String[]) obj; + } else if (obj instanceof Object[]) { + Object[] ol = (Object[]) obj; seedHosts = Arrays.copyOf(ol, ol.length, String[].class); } - return new Cluster(r.getString("name"), r.getString("partitioner"), Sets.newHashSet(seedHosts)); + return new Cluster(rs.getString("name"), rs.getString("partitioner"), Sets.newHashSet(seedHosts)); } - } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSql.java similarity index 51% rename from src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java rename to src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSql.java index 1e07f78af..7641be763 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSql.java @@ -11,8 +11,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSchedule; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.view.RepairRunStatus; +import com.spotify.reaper.resources.view.RepairScheduleStatus; +import com.spotify.reaper.service.RepairParameters; + import java.math.BigInteger; import java.util.Collection; import java.util.Iterator; @@ -27,266 +37,269 @@ import org.skife.jdbi.v2.sqlobject.customizers.BatchChunkSize; import org.skife.jdbi.v2.sqlobject.customizers.Mapper; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSchedule; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.view.RepairRunStatus; -import com.spotify.reaper.resources.view.RepairScheduleStatus; -import com.spotify.reaper.service.RepairParameters; - /** * JDBI based PostgreSQL interface. * + *

* See following specification for more info: http://jdbi.org/sql_object_api_dml/ */ -public interface IStoragePostgreSQL { +public interface IStoragePostgreSql { // Cluster // String SQL_CLUSTER_ALL_FIELDS = "name, partitioner, seed_hosts"; String SQL_GET_ALL_CLUSTERS = "SELECT " + SQL_CLUSTER_ALL_FIELDS + " FROM cluster"; - String SQL_GET_CLUSTER = - "SELECT " + SQL_CLUSTER_ALL_FIELDS + " FROM cluster WHERE name = :name"; + String SQL_GET_CLUSTER = "SELECT " + SQL_CLUSTER_ALL_FIELDS + " FROM cluster WHERE name = :name"; String SQL_INSERT_CLUSTER = "INSERT INTO cluster (" + SQL_CLUSTER_ALL_FIELDS - + ") VALUES (:name, :partitioner, :seedHosts)"; + + ") VALUES (:name, :partitioner, :seedHosts)"; String SQL_UPDATE_CLUSTER = "UPDATE cluster SET partitioner = :partitioner, " - + "seed_hosts = :seedHosts WHERE name = :name"; + + "seed_hosts = :seedHosts WHERE name = :name"; String SQL_DELETE_CLUSTER = "DELETE FROM cluster WHERE name = :name"; // RepairRun // - String SQL_REPAIR_RUN_ALL_FIELDS_NO_ID = - "cluster_name, repair_unit_id, cause, owner, state, creation_time, " + String SQL_REPAIR_RUN_ALL_FIELDS_NO_ID = "cluster_name, repair_unit_id, cause, owner, state, creation_time, " + "start_time, end_time, pause_time, intensity, last_event, " + "segment_count, repair_parallelism"; - String SQL_REPAIR_RUN_ALL_FIELDS = - "repair_run.id, " + SQL_REPAIR_RUN_ALL_FIELDS_NO_ID; - String SQL_INSERT_REPAIR_RUN = - "INSERT INTO repair_run (" + SQL_REPAIR_RUN_ALL_FIELDS_NO_ID + ") VALUES " + String SQL_REPAIR_RUN_ALL_FIELDS = "repair_run.id, " + SQL_REPAIR_RUN_ALL_FIELDS_NO_ID; + String SQL_INSERT_REPAIR_RUN = "INSERT INTO repair_run (" + + SQL_REPAIR_RUN_ALL_FIELDS_NO_ID + + ") VALUES " + "(:clusterName, :repairUnitId, :cause, :owner, :runState, :creationTime, " + ":startTime, :endTime, :pauseTime, :intensity, :lastEvent, :segmentCount, " + ":repairParallelism)"; - String SQL_UPDATE_REPAIR_RUN = - "UPDATE repair_run SET cause = :cause, owner = :owner, state = :runState, " + String SQL_UPDATE_REPAIR_RUN = "UPDATE repair_run SET cause = :cause, owner = :owner, state = :runState, " + "start_time = :startTime, end_time = :endTime, pause_time = :pauseTime, " + "intensity = :intensity, last_event = :lastEvent, segment_count = :segmentCount, " + "repair_parallelism = :repairParallelism WHERE id = :id"; - String SQL_GET_REPAIR_RUN = - "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE id = :id"; - String SQL_GET_REPAIR_RUNS_FOR_CLUSTER = - "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE cluster_name = :clusterName"; - String SQL_GET_REPAIR_RUNS_WITH_STATE = - "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE state = :state"; - String SQL_GET_REPAIR_RUNS_FOR_UNIT = - "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE repair_unit_id = :unitId"; + String SQL_GET_REPAIR_RUN = "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE id = :id"; + String SQL_GET_REPAIR_RUNS_FOR_CLUSTER = "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + + " FROM repair_run WHERE cluster_name = :clusterName"; + String SQL_GET_REPAIR_RUNS_WITH_STATE = "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + + " FROM repair_run WHERE state = :state"; + String SQL_GET_REPAIR_RUNS_FOR_UNIT = "SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + + " FROM repair_run WHERE repair_unit_id = :unitId"; String SQL_DELETE_REPAIR_RUN = "DELETE FROM repair_run WHERE id = :id"; - // RepairUnit + // RepairUni // - static final String SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID = - "cluster_name, keyspace_name, column_families, incremental_repair, nodes, datacenters"; - static final String SQL_REPAIR_UNIT_ALL_FIELDS = - "repair_unit.id, " + SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID; - static final String SQL_INSERT_REPAIR_UNIT = - "INSERT INTO repair_unit (" + SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID + ") VALUES " - + "(:clusterName, :keyspaceName, :columnFamilies, :incrementalRepair, :nodes, :datacenters)"; - static final String SQL_GET_REPAIR_UNIT = - - "SELECT " + SQL_REPAIR_UNIT_ALL_FIELDS + " FROM repair_unit WHERE id = :id"; - String SQL_GET_REPAIR_UNIT_BY_CLUSTER_AND_TABLES = - "SELECT " + SQL_REPAIR_UNIT_ALL_FIELDS + " FROM repair_unit " + String SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID + = "cluster_name, keyspace_name, column_families, incremental_repair, nodes, datacenters"; + String SQL_REPAIR_UNIT_ALL_FIELDS = "repair_unit.id, " + SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID; + String SQL_INSERT_REPAIR_UNIT = "INSERT INTO repair_unit (" + + SQL_REPAIR_UNIT_ALL_FIELDS_NO_ID + + ") VALUES " + + "(:clusterName, :keyspaceName, :columnFamilies, :incrementalRepair, :nodes, :datacenters)"; + String SQL_GET_REPAIR_UNIT = "SELECT " + SQL_REPAIR_UNIT_ALL_FIELDS + " FROM repair_unit WHERE id = :id"; + + String SQL_GET_REPAIR_UNIT_BY_CLUSTER_AND_TABLES = "SELECT " + + SQL_REPAIR_UNIT_ALL_FIELDS + + " FROM repair_unit " + "WHERE cluster_name = :clusterName AND keyspace_name = :keyspaceName " + "AND column_families = :columnFamilies"; - static final String SQL_DELETE_REPAIR_UNIT = "DELETE FROM repair_unit WHERE id = :id"; + String SQL_DELETE_REPAIR_UNIT = "DELETE FROM repair_unit WHERE id = :id"; - // RepairSegment + // RepairSegmen // - String SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID = - "repair_unit_id, run_id, start_token, end_token, state, coordinator_host, start_time, " + String SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID + = "repair_unit_id, run_id, start_token, end_token, state, coordinator_host, start_time, " + "end_time, fail_count"; - String SQL_REPAIR_SEGMENT_ALL_FIELDS = - "repair_segment.id, " + SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID; - String SQL_INSERT_REPAIR_SEGMENT = - "INSERT INTO repair_segment (" + SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID + ") VALUES " + String SQL_REPAIR_SEGMENT_ALL_FIELDS = "repair_segment.id, " + SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID; + String SQL_INSERT_REPAIR_SEGMENT = "INSERT INTO repair_segment (" + + SQL_REPAIR_SEGMENT_ALL_FIELDS_NO_ID + + ") VALUES " + "(:repairUnitId, :runId, :startToken, :endToken, :state, :coordinatorHost, :startTime, " + ":endTime, :failCount)"; - String SQL_UPDATE_REPAIR_SEGMENT = - "UPDATE repair_segment SET repair_unit_id = :repairUnitId, run_id = :runId, " + String SQL_UPDATE_REPAIR_SEGMENT = "UPDATE repair_segment SET repair_unit_id = :repairUnitId, run_id = :runId, " + "start_token = :startToken, end_token = :endToken, state = :state, " + "coordinator_host = :coordinatorHost, start_time = :startTime, end_time = :endTime, " + "fail_count = :failCount WHERE id = :id"; - String SQL_GET_REPAIR_SEGMENT = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE id = :id"; - String SQL_GET_REPAIR_SEGMENTS_FOR_RUN = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE run_id = :runId"; - String SQL_GET_REPAIR_SEGMENTS_FOR_RUN_WITH_STATE = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE " - + "run_id = :runId AND state = :state"; - String SQL_GET_RUNNING_REPAIRS_FOR_CLUSTER = - "SELECT start_token, end_token, keyspace_name, column_families, repair_parallelism " - + "FROM repair_segment " - + "JOIN repair_run ON run_id = repair_run.id " - + "JOIN repair_unit ON repair_run.repair_unit_id = repair_unit.id " - + "WHERE repair_segment.state = 1 AND repair_unit.cluster_name = :clusterName"; - String SQL_GET_NEXT_FREE_REPAIR_SEGMENT = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE run_id = :runId " + String SQL_GET_REPAIR_SEGMENT = "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE id = :id"; + String SQL_GET_REPAIR_SEGMENTS_FOR_RUN = "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + + " FROM repair_segment WHERE run_id = :runId"; + String SQL_GET_REPAIR_SEGMENTS_FOR_RUN_WITH_STATE = "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + + " FROM repair_segment WHERE " + "run_id = :runId AND state = :state"; + String SQL_GET_RUNNING_REPAIRS_FOR_CLUSTER + = "SELECT start_token, end_token, keyspace_name, column_families, repair_parallelism " + + "FROM repair_segment " + + "JOIN repair_run ON run_id = repair_run.id " + + "JOIN repair_unit ON repair_run.repair_unit_id = repair_unit.id " + + "WHERE repair_segment.state = 1 AND repair_unit.cluster_name = :clusterName"; + String SQL_GET_NEXT_FREE_REPAIR_SEGMENT = "SELECT " + + SQL_REPAIR_SEGMENT_ALL_FIELDS + + " FROM repair_segment WHERE run_id = :runId " + "AND state = 0 ORDER BY fail_count ASC, start_token ASC LIMIT 1"; - String SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_NON_WRAPPING_RANGE = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE " + String SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_NON_WRAPPING_RANGE = "SELECT " + + SQL_REPAIR_SEGMENT_ALL_FIELDS + + " FROM repair_segment WHERE " + "run_id = :runId AND state = 0 AND start_token < end_token AND " + "(start_token >= :startToken AND end_token <= :endToken) " + "ORDER BY fail_count ASC, start_token ASC LIMIT 1"; - String SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_WRAPPING_RANGE = - "SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE " + String SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_WRAPPING_RANGE = "SELECT " + + SQL_REPAIR_SEGMENT_ALL_FIELDS + + " FROM repair_segment WHERE " + "run_id = :runId AND state = 0 AND " + "((start_token < end_token AND (start_token >= :startToken OR end_token <= :endToken)) OR " + "(start_token >= :startToken AND end_token <= :endToken)) " + "ORDER BY fail_count ASC, start_token ASC LIMIT 1"; - String SQL_DELETE_REPAIR_SEGMENTS_FOR_RUN = - "DELETE FROM repair_segment WHERE run_id = :runId"; + String SQL_DELETE_REPAIR_SEGMENTS_FOR_RUN = "DELETE FROM repair_segment WHERE run_id = :runId"; // RepairSchedule // - String SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID = - "repair_unit_id, state, days_between, next_activation, run_history, segment_count, " + String SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID + = "repair_unit_id, state, days_between, next_activation, run_history, segment_count, " + "repair_parallelism, intensity, creation_time, owner, pause_time"; - String SQL_REPAIR_SCHEDULE_ALL_FIELDS = - "repair_schedule.id, " + SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID; - String SQL_INSERT_REPAIR_SCHEDULE = - "INSERT INTO repair_schedule (" + SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID + ") VALUES " - + "(:repairUnitId, :state, :daysBetween, :nextActivation, :runHistorySQL, :segmentCount, " + String SQL_REPAIR_SCHEDULE_ALL_FIELDS = "repair_schedule.id, " + SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID; + String SQL_INSERT_REPAIR_SCHEDULE = "INSERT INTO repair_schedule (" + + SQL_REPAIR_SCHEDULE_ALL_FIELDS_NO_ID + + ") VALUES " + + "(:repairUnitId, :state, :daysBetween, :nextActivation, :runHistorySql, :segmentCount, " + ":repairParallelism, :intensity, :creationTime, :owner, :pauseTime)"; - String SQL_UPDATE_REPAIR_SCHEDULE = - "UPDATE repair_schedule SET repair_unit_id = :repairUnitId, state = :state, " + String SQL_UPDATE_REPAIR_SCHEDULE = "UPDATE repair_schedule SET repair_unit_id = :repairUnitId, state = :state, " + "days_between = :daysBetween, next_activation = :nextActivation, " - + "run_history = :runHistorySQL, segment_count = :segmentCount, " + + "run_history = :runHistorySql, segment_count = :segmentCount, " + "repair_parallelism = :repairParallelism, creation_time = :creationTime, owner = :owner, " + "pause_time = :pauseTime WHERE id = :id"; - String SQL_GET_REPAIR_SCHEDULE = - "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule WHERE id = :id"; - String SQL_GET_REPAIR_SCHEDULES_FOR_CLUSTER = - "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule, repair_unit " + String SQL_GET_REPAIR_SCHEDULE = "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule WHERE id = :id"; + String SQL_GET_REPAIR_SCHEDULES_FOR_CLUSTER = "SELECT " + + SQL_REPAIR_SCHEDULE_ALL_FIELDS + + " FROM repair_schedule, repair_unit " + "WHERE repair_schedule.repair_unit_id = repair_unit.id AND cluster_name = :clusterName"; - String SQL_GET_REPAIR_SCHEDULES_FOR_KEYSPACE = - "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule, repair_unit " + String SQL_GET_REPAIR_SCHEDULES_FOR_KEYSPACE = "SELECT " + + SQL_REPAIR_SCHEDULE_ALL_FIELDS + + " FROM repair_schedule, repair_unit " + "WHERE repair_schedule.repair_unit_id = repair_unit.id AND keyspace_name = :keyspaceName"; - String SQL_GET_REPAIR_SCHEDULES_FOR_CLUSTER_AND_KEYSPACE = - "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule, repair_unit " + String SQL_GET_REPAIR_SCHEDULES_FOR_CLUSTER_AND_KEYSPACE = "SELECT " + + SQL_REPAIR_SCHEDULE_ALL_FIELDS + + " FROM repair_schedule, repair_unit " + "WHERE repair_schedule.repair_unit_id = repair_unit.id AND cluster_name = :clusterName " + "AND keyspace_name = :keyspaceName"; - String SQL_GET_ALL_REPAIR_SCHEDULES = - "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule"; + String SQL_GET_ALL_REPAIR_SCHEDULES = "SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule"; String SQL_DELETE_REPAIR_SCHEDULE = "DELETE FROM repair_schedule WHERE id = :id"; // Utility methods // - String SQL_GET_REPAIR_RUN_IDS_FOR_CLUSTER = - "SELECT id FROM repair_run WHERE cluster_name = :clusterName"; - String SQL_SEGMENT_AMOUNT_FOR_REPAIR_RUN = - "SELECT count(*) FROM repair_segment WHERE run_id = :runId"; - String SQL_SEGMENT_AMOUNT_FOR_REPAIR_RUN_WITH_STATE = - "SELECT count(*) FROM repair_segment WHERE run_id = :runId AND state = :state"; + String SQL_GET_REPAIR_RUN_IDS_FOR_CLUSTER = "SELECT id FROM repair_run WHERE cluster_name = :clusterName"; + String SQL_SEGMENT_AMOUNT_FOR_REPAIR_RUN = "SELECT count(*) FROM repair_segment WHERE run_id = :runId"; + String SQL_SEGMENT_AMOUNT_FOR_REPAIR_RUN_WITH_STATE + = "SELECT count(*) FROM repair_segment WHERE run_id = :runId AND state = :state"; // View-specific queries // - String SQL_CLUSTER_RUN_OVERVIEW = - "SELECT repair_run.id, repair_unit.cluster_name, keyspace_name, column_families, " - + "(SELECT COUNT(case when state = 2 then 1 else null end) FROM repair_segment WHERE run_id = repair_run.id) AS segments_repaired, " - + "(SELECT COUNT(*) FROM repair_segment WHERE run_id = repair_run.id) AS segments_total, " - + "repair_run.state, repair_run.start_time, " - + "repair_run.end_time, cause, owner, last_event, " - + "creation_time, pause_time, intensity, repair_parallelism, incremental_repair " - + "FROM repair_run " - + "JOIN repair_unit ON repair_unit_id = repair_unit.id " - + "WHERE repair_unit.cluster_name = :clusterName " - + "ORDER BY COALESCE(end_time, start_time) DESC, start_time DESC " - + "LIMIT :limit"; - - static final String SQL_CLUSTER_SCHEDULE_OVERVIEW = - "SELECT repair_schedule.id, owner, cluster_name, keyspace_name, column_families, state, " - + "creation_time, next_activation, pause_time, intensity, segment_count, " - + "repair_parallelism, days_between, incremental_repair " - + "FROM repair_schedule " - + "JOIN repair_unit ON repair_unit_id = repair_unit.id " - + "WHERE cluster_name = :clusterName"; + String SQL_CLUSTER_RUN_OVERVIEW = "SELECT repair_run.id, repair_unit.cluster_name, keyspace_name, column_families, " + + "(SELECT COUNT(case when state = 2 then 1 else null end) FROM repair_segment " + + "WHERE run_id = repair_run.id) AS segments_repaired, " + + "(SELECT COUNT(*) FROM repair_segment WHERE run_id = repair_run.id) AS segments_total, " + + "repair_run.state, repair_run.start_time, " + + "repair_run.end_time, cause, owner, last_event, " + + "creation_time, pause_time, intensity, repair_parallelism, incremental_repair " + + "FROM repair_run " + + "JOIN repair_unit ON repair_unit_id = repair_unit.id " + + "WHERE repair_unit.cluster_name = :clusterName " + + "ORDER BY COALESCE(end_time, start_time) DESC, start_time DESC " + + "LIMIT :limit"; + + String SQL_CLUSTER_SCHEDULE_OVERVIEW + = "SELECT repair_schedule.id, owner, cluster_name, keyspace_name, column_families, state, " + + "creation_time, next_activation, pause_time, intensity, segment_count, " + + "repair_parallelism, days_between, incremental_repair " + + "FROM repair_schedule " + + "JOIN repair_unit ON repair_unit_id = repair_unit.id " + + "WHERE cluster_name = :clusterName"; @SqlQuery("SELECT CURRENT_TIMESTAMP") String getCurrentDate(); @SqlQuery(SQL_GET_CLUSTER) @Mapper(ClusterMapper.class) - Cluster getCluster(@Bind("name") String clusterName); + Cluster getCluster( + @Bind("name") String clusterName); @SqlQuery(SQL_GET_ALL_CLUSTERS) @Mapper(ClusterMapper.class) Collection getClusters(); @SqlUpdate(SQL_INSERT_CLUSTER) - int insertCluster(@BindBean Cluster newCluster); + int insertCluster( + @BindBean Cluster newCluster); @SqlUpdate(SQL_UPDATE_CLUSTER) - int updateCluster(@BindBean Cluster newCluster); + int updateCluster( + @BindBean Cluster newCluster); @SqlUpdate(SQL_DELETE_CLUSTER) - int deleteCluster(@Bind("name") String clusterName); + int deleteCluster( + @Bind("name") String clusterName); @SqlQuery(SQL_GET_REPAIR_RUN) @Mapper(RepairRunMapper.class) - RepairRun getRepairRun(@Bind("id") long repairRunId); + RepairRun getRepairRun( + @Bind("id") long repairRunId); @SqlQuery(SQL_GET_REPAIR_RUNS_FOR_CLUSTER) @Mapper(RepairRunMapper.class) - Collection getRepairRunsForCluster(@Bind("clusterName") String clusterName); + Collection getRepairRunsForCluster( + @Bind("clusterName") String clusterName); @SqlQuery(SQL_GET_REPAIR_RUNS_WITH_STATE) @Mapper(RepairRunMapper.class) - Collection getRepairRunsWithState(@Bind("state") RepairRun.RunState state); + Collection getRepairRunsWithState( + @Bind("state") RepairRun.RunState state); @SqlQuery(SQL_GET_REPAIR_RUNS_FOR_UNIT) @Mapper(RepairRunMapper.class) - Collection getRepairRunsForUnit(@Bind("unitId") long unitId); + Collection getRepairRunsForUnit( + @Bind("unitId") long unitId); @SqlUpdate(SQL_INSERT_REPAIR_RUN) @GetGeneratedKeys - long insertRepairRun(@BindBean RepairRun newRepairRun); + long insertRepairRun( + @BindBean RepairRun newRepairRun); @SqlUpdate(SQL_UPDATE_REPAIR_RUN) - int updateRepairRun(@BindBean RepairRun newRepairRun); + int updateRepairRun( + @BindBean RepairRun newRepairRun); @SqlUpdate(SQL_DELETE_REPAIR_RUN) - int deleteRepairRun(@Bind("id") long repairRunId); + int deleteRepairRun( + @Bind("id") long repairRunId); @SqlQuery(SQL_GET_REPAIR_UNIT) @Mapper(RepairUnitMapper.class) - RepairUnit getRepairUnit(@Bind("id") long repairUnitId); + RepairUnit getRepairUnit( + @Bind("id") long repairUnitId); @SqlQuery(SQL_GET_REPAIR_UNIT_BY_CLUSTER_AND_TABLES) @Mapper(RepairUnitMapper.class) - RepairUnit getRepairUnitByClusterAndTables(@Bind("clusterName") String clusterName, + RepairUnit getRepairUnitByClusterAndTables( + @Bind("clusterName") String clusterName, @Bind("keyspaceName") String keyspaceName, @Bind("columnFamilies") Collection columnFamilies); @SqlUpdate(SQL_INSERT_REPAIR_UNIT) @GetGeneratedKeys - long insertRepairUnit(@BindBean RepairUnit newRepairUnit); + long insertRepairUnit( + @BindBean RepairUnit newRepairUnit); @SqlUpdate(SQL_DELETE_REPAIR_UNIT) - int deleteRepairUnit(@Bind("id") long repairUnitId); + int deleteRepairUnit( + @Bind("id") long repairUnitId); @SqlBatch(SQL_INSERT_REPAIR_SEGMENT) @BatchChunkSize(500) - void insertRepairSegments(@BindBean Iterator newRepairSegments); + void insertRepairSegments( + @BindBean Iterator newRepairSegments); @SqlUpdate(SQL_UPDATE_REPAIR_SEGMENT) - int updateRepairSegment(@BindBean RepairSegment newRepairSegment); + int updateRepairSegment( + @BindBean RepairSegment newRepairSegment); @SqlQuery(SQL_GET_REPAIR_SEGMENT) @Mapper(RepairSegmentMapper.class) - RepairSegment getRepairSegment(@Bind("id") long repairSegmentId); + RepairSegment getRepairSegment( + @Bind("id") long repairSegmentId); @SqlQuery(SQL_GET_REPAIR_SEGMENTS_FOR_RUN) @Mapper(RepairSegmentMapper.class) @@ -306,33 +319,40 @@ Collection getRunningRepairsForCluster( @SqlQuery(SQL_GET_NEXT_FREE_REPAIR_SEGMENT) @Mapper(RepairSegmentMapper.class) - RepairSegment getNextFreeRepairSegment(@Bind("runId") long runId); + RepairSegment getNextFreeRepairSegment( + @Bind("runId") long runId); @SqlQuery(SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_NON_WRAPPING_RANGE) @Mapper(RepairSegmentMapper.class) - RepairSegment getNextFreeRepairSegmentInNonWrappingRange(@Bind("runId") long runId, + RepairSegment getNextFreeRepairSegmentInNonWrappingRange( + @Bind("runId") long runId, @Bind("startToken") BigInteger startToken, @Bind("endToken") BigInteger endToken); @SqlQuery(SQL_GET_NEXT_FREE_REPAIR_SEGMENT_IN_WRAPPING_RANGE) @Mapper(RepairSegmentMapper.class) - RepairSegment getNextFreeRepairSegmentInWrappingRange(@Bind("runId") long runId, + RepairSegment getNextFreeRepairSegmentInWrappingRange( + @Bind("runId") long runId, @Bind("startToken") BigInteger startToken, @Bind("endToken") BigInteger endToken); @SqlUpdate(SQL_DELETE_REPAIR_SEGMENTS_FOR_RUN) - int deleteRepairSegmentsForRun(@Bind("runId") long repairRunId); + int deleteRepairSegmentsForRun( + @Bind("runId") long repairRunId); @SqlQuery(SQL_GET_REPAIR_SCHEDULE) @Mapper(RepairScheduleMapper.class) - RepairSchedule getRepairSchedule(@Bind("id") long repairScheduleId); + RepairSchedule getRepairSchedule( + @Bind("id") long repairScheduleId); @SqlUpdate(SQL_INSERT_REPAIR_SCHEDULE) @GetGeneratedKeys - long insertRepairSchedule(@BindBean RepairSchedule newRepairSchedule); + long insertRepairSchedule( + @BindBean RepairSchedule newRepairSchedule); @SqlUpdate(SQL_UPDATE_REPAIR_SCHEDULE) - int updateRepairSchedule(@BindBean RepairSchedule newRepairSchedule); + int updateRepairSchedule( + @BindBean RepairSchedule newRepairSchedule); @SqlQuery(SQL_GET_REPAIR_SCHEDULES_FOR_CLUSTER) @Mapper(RepairScheduleMapper.class) @@ -359,7 +379,8 @@ Collection getRepairRunIdsForCluster( @Bind("clusterName") String clusterName); @SqlUpdate(SQL_DELETE_REPAIR_SCHEDULE) - int deleteRepairSchedule(@Bind("id") long repairScheduleId); + int deleteRepairSchedule( + @Bind("id") long repairScheduleId); @SqlQuery(SQL_SEGMENT_AMOUNT_FOR_REPAIR_RUN) int getSegmentAmountForRepairRun( @@ -370,7 +391,6 @@ int getSegmentAmountForRepairRunWithState( @Bind("runId") long runId, @Bind("state") RepairSegment.State state); - @SqlQuery(SQL_CLUSTER_RUN_OVERVIEW) @Mapper(RepairRunStatusMapper.class) List getClusterRunOverview( diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSQLTypeArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSQLTypeArgumentFactory.java deleted file mode 100644 index c245fff3d..000000000 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSQLTypeArgumentFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.spotify.reaper.storage.postgresql; - -import com.spotify.reaper.core.RepairSchedule.LongCollectionSQLType; -import org.skife.jdbi.v2.StatementContext; -import org.skife.jdbi.v2.tweak.Argument; -import org.skife.jdbi.v2.tweak.ArgumentFactory; - -import java.sql.Array; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -/** - * Provides JDBI a method to map our custom Long collection into an SQL Array type. - * - * NOTICE: this is very ugly due to not being able to have different generic types - * except Strings when using Collections with JDBI here. - */ -public final class LongCollectionSQLTypeArgumentFactory implements ArgumentFactory { - - @Override - public boolean accepts(Class expectedType, Object value, StatementContext ctx) { - return value instanceof LongCollectionSQLType; - } - - @Override - public Argument build(Class expectedType, final LongCollectionSQLType value, StatementContext ctx) { - return (int position, PreparedStatement statement, StatementContext ctx1) -> { - try { - Array sqlArray = ctx1.getConnection().createArrayOf("int", value.getValue().toArray()); - statement.setArray(position, sqlArray); - }catch(SQLException e) { - // H2 DB feature not supported: "createArray" error - if(e.getErrorCode() != 50100) { - throw e; - } - statement.setObject(position, value.getValue().toArray()); - } - }; - } -} diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSqlTypeArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSqlTypeArgumentFactory.java new file mode 100644 index 000000000..ee9a822a4 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/LongCollectionSqlTypeArgumentFactory.java @@ -0,0 +1,56 @@ +/* + * 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.spotify.reaper.storage.postgresql; + +import com.spotify.reaper.core.RepairSchedule.LongCollectionSqlType; + +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +/** + * Provides JDBI a method to map our custom Long collection into an SQL Array type. + * + *

+ * NOTICE: this is very ugly due to not being able to have different generic types except Strings when using + * Collections with JDBI here. + */ +public final class LongCollectionSqlTypeArgumentFactory implements ArgumentFactory { + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof LongCollectionSqlType; + } + + @Override + public Argument build(Class expectedType, final LongCollectionSqlType value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + try { + Array sqlArray = ctx1.getConnection().createArrayOf("int", value.getValue().toArray()); + statement.setArray(position, sqlArray); + } catch (SQLException e) { + // H2 DB feature not supported: "createArray" error + if (e.getErrorCode() != 50100) { + throw e; + } + statement.setObject(position, value.getValue().toArray()); + } + }; + } +} diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/PostgresArrayArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/PostgresArrayArgumentFactory.java index ec75bb557..05ed5254f 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/PostgresArrayArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/PostgresArrayArgumentFactory.java @@ -11,26 +11,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper.storage.postgresql; -import org.skife.jdbi.v2.StatementContext; -import org.skife.jdbi.v2.tweak.Argument; -import org.skife.jdbi.v2.tweak.ArgumentFactory; +package com.spotify.reaper.storage.postgresql; import java.sql.Array; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Collection; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + /** * Provides JDBI a method to map String Collection to an SQL Array type. * - * NOTICE: this is very non-generic and ugly due to not being able to have different generic types - * except Strings when using Collections with JDBI here. - * Should probably use own collection types without generics to solve this. - * See LongCollectionSQLType for example, if this becomes a problem. + *

+ * NOTICE: this is very non-generic and ugly due to not being able to have different generic types except Strings + * when using Collections with JDBI here. Should probably use own collection types without generics to solve this. See + * LongCollectionSQLType for example, if this becomes a problem. */ -public class PostgresArrayArgumentFactory implements ArgumentFactory> { +public final class PostgresArrayArgumentFactory implements ArgumentFactory> { @Override public boolean accepts(Class expectedType, Object value, StatementContext ctx) { @@ -38,21 +39,17 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx } @Override - public Argument build(Class expectedType, final Collection value, - StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - try { - Array sqlArray = ctx.getConnection().createArrayOf("text", value.toArray()); - statement.setArray(position, sqlArray); - } catch(SQLException e) { - // H2 DB feature not supported: "createArray" error - if(e.getErrorCode() != 50100) { - throw e; - } - statement.setObject(position, value.toArray()); + public Argument build(Class expectedType, final Collection value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + try { + Array sqlArray = ctx1.getConnection().createArrayOf("text", value.toArray()); + statement.setArray(position, sqlArray); + } catch (SQLException e) { + // H2 DB feature not supported: "createArray" error + if (e.getErrorCode() != 50100) { + throw e; } + statement.setObject(position, value.toArray()); } }; } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParallelismArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParallelismArgumentFactory.java index 19e072748..df1243341 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParallelismArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParallelismArgumentFactory.java @@ -11,20 +11,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; + +import java.sql.PreparedStatement; + import org.apache.cassandra.repair.RepairParallelism; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; -import java.sql.PreparedStatement; -import java.sql.SQLException; - /** * Provides JDBI a method to map RepairParallelism value to a TEXT value in database. */ -public class RepairParallelismArgumentFactory implements ArgumentFactory { +public final class RepairParallelismArgumentFactory implements ArgumentFactory { @Override public boolean accepts(Class expectedType, Object value, StatementContext ctx) { @@ -32,13 +33,9 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx } @Override - public Argument build(Class expectedType, final RepairParallelism value, - StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - statement.setString(position, value.toString()); - } + public Argument build(Class expectedType, final RepairParallelism value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + statement.setString(position, value.toString()); }; } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParametersMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParametersMapper.java index d3b61d7ac..4c715a7c9 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParametersMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairParametersMapper.java @@ -11,45 +11,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; -import com.google.common.collect.Sets; import com.spotify.reaper.service.RepairParameters; import com.spotify.reaper.service.RingRange; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; + +import com.google.common.collect.Sets; import org.apache.cassandra.repair.RepairParallelism; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Arrays; +public final class RepairParametersMapper implements ResultSetMapper { -public class RepairParametersMapper implements ResultSetMapper { @Override - public RepairParameters map(int index, ResultSet r, StatementContext ctx) throws SQLException { - RingRange range = new RingRange(r.getBigDecimal("start_token").toBigInteger(), - r.getBigDecimal("end_token").toBigInteger()); - Object columnFamiliesObj = r.getArray("column_families").getArray(); + public RepairParameters map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + + RingRange range + = new RingRange(rs.getBigDecimal("start_token").toBigInteger(), rs.getBigDecimal("end_token").toBigInteger()); + + Object columnFamiliesObj = rs.getArray("column_families").getArray(); String[] columnFamilies; if (columnFamiliesObj instanceof String[]) { - columnFamilies = (String[]) columnFamiliesObj; + columnFamilies = (String[]) columnFamiliesObj; } else { - Object[] objArray = (Object[]) columnFamiliesObj; - columnFamilies = Arrays.copyOf(objArray, objArray.length, String[].class); + Object[] objArray = (Object[]) columnFamiliesObj; + columnFamilies = Arrays.copyOf(objArray, objArray.length, String[].class); } - String repairParallelismStr = r.getString("repair_parallelism"); - if (repairParallelismStr != null) - { + String repairParallelismStr = rs.getString("repair_parallelism"); + if (repairParallelismStr != null) { repairParallelismStr = repairParallelismStr.toUpperCase(); } RepairParallelism repairParallelism = RepairParallelism.fromName(repairParallelismStr); - return new RepairParameters(range, - r.getString("keyspace_name"), - Sets.newHashSet(columnFamilies), - repairParallelism); + return new RepairParameters( + range, + rs.getString("keyspace_name"), + Sets.newHashSet(columnFamilies), + repairParallelism); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunMapper.java index ebf7cf20e..455f4e142 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunMapper.java @@ -11,23 +11,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; import com.spotify.reaper.core.RepairRun; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; + import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; - -public class RepairRunMapper implements ResultSetMapper { +public final class RepairRunMapper implements ResultSetMapper { - static DateTime getDateTimeOrNull(ResultSet r, String dbColumnName) throws SQLException { - Timestamp timestamp = r.getTimestamp(dbColumnName); + static DateTime getDateTimeOrNull(ResultSet rs, String dbColumnName) throws SQLException { + Timestamp timestamp = rs.getTimestamp(dbColumnName); DateTime result = null; if (null != timestamp) { result = new DateTime(timestamp); @@ -35,25 +36,28 @@ static DateTime getDateTimeOrNull(ResultSet r, String dbColumnName) throws SQLEx return result; } - public RepairRun map(int index, ResultSet r, StatementContext ctx) throws SQLException { - RepairRun.RunState runState = RepairRun.RunState.valueOf(r.getString("state")); - RepairParallelism repairParallelism = - RepairParallelism.fromName(r.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")); - RepairRun.Builder repairRunBuilder = - new RepairRun.Builder(r.getString("cluster_name"), - UuidUtil.fromSequenceId(r.getLong("repair_unit_id")), - getDateTimeOrNull(r, "creation_time"), - r.getFloat("intensity"), - r.getInt("segment_count"), - repairParallelism); + @Override + public RepairRun map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + RepairRun.RunState runState = RepairRun.RunState.valueOf(rs.getString("state")); + RepairParallelism repairParallelism = RepairParallelism.fromName( + rs.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")); + + RepairRun.Builder repairRunBuilder = new RepairRun.Builder( + rs.getString("cluster_name"), + UuidUtil.fromSequenceId(rs.getLong("repair_unit_id")), + getDateTimeOrNull(rs, "creation_time"), + rs.getFloat("intensity"), + rs.getInt("segment_count"), + repairParallelism); + return repairRunBuilder .runState(runState) - .owner(r.getString("owner")) - .cause(r.getString("cause")) - .startTime(getDateTimeOrNull(r, "start_time")) - .endTime(getDateTimeOrNull(r, "end_time")) - .pauseTime(getDateTimeOrNull(r, "pause_time")) - .lastEvent(r.getString("last_event")) - .build(UuidUtil.fromSequenceId(r.getLong("id"))); + .owner(rs.getString("owner")) + .cause(rs.getString("cause")) + .startTime(getDateTimeOrNull(rs, "start_time")) + .endTime(getDateTimeOrNull(rs, "end_time")) + .pauseTime(getDateTimeOrNull(rs, "pause_time")) + .lastEvent(rs.getString("last_event")) + .build(UuidUtil.fromSequenceId(rs.getLong("id"))); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunStatusMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunStatusMapper.java index 42c70dc03..4ec5b1f3a 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunStatusMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairRunStatusMapper.java @@ -1,46 +1,78 @@ +/* + * 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.spotify.reaper.storage.postgresql; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.resources.view.RepairRunStatus; + import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; +import com.google.common.collect.ImmutableSet; import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import com.google.common.collect.ImmutableSet; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.resources.view.RepairRunStatus; -public class RepairRunStatusMapper implements ResultSetMapper { +public final class RepairRunStatusMapper implements ResultSetMapper { @Override - public RepairRunStatus map(int index, ResultSet r, StatementContext ctx) throws SQLException { - long runId = r.getLong("id"); - String clusterName = r.getString("cluster_name"); - String keyspaceName = r.getString("keyspace_name"); - Collection columnFamilies = - ImmutableSet.copyOf((String[]) r.getArray("column_families").getArray()); - int segmentsRepaired = r.getInt("segments_repaired"); - int totalSegments = r.getInt("segments_total"); - RepairRun.RunState state = RepairRun.RunState.valueOf(r.getString("state")); - DateTime startTime = RepairRunMapper.getDateTimeOrNull(r, "start_time"); - DateTime endTime = RepairRunMapper.getDateTimeOrNull(r, "end_time"); - String cause = r.getString("cause"); - String owner = r.getString("owner"); - String lastEvent = r.getString("last_event"); - DateTime creationTime = RepairRunMapper.getDateTimeOrNull(r, "creation_time"); - DateTime pauseTime = RepairRunMapper.getDateTimeOrNull(r, "pause_time"); - Double intensity = r.getDouble("intensity"); - Boolean incrementalRepair = r.getBoolean("incremental_repair"); - RepairParallelism repairParallelism = - RepairParallelism.fromName(r.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")); - Collection nodes = ImmutableSet.copyOf((String[]) r.getArray("nodes").getArray()); - Collection datacenters = ImmutableSet.copyOf((String[]) r.getArray("datacenters").getArray()); - - return new RepairRunStatus(UuidUtil.fromSequenceId(runId), clusterName, keyspaceName, columnFamilies, segmentsRepaired, - totalSegments, state, startTime, endTime, cause, owner, lastEvent, - creationTime, pauseTime, intensity, incrementalRepair, repairParallelism, nodes, datacenters); + public RepairRunStatus map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + long runId = rs.getLong("id"); + String clusterName = rs.getString("cluster_name"); + String keyspaceName = rs.getString("keyspace_name"); + Collection columnFamilies = ImmutableSet.copyOf((String[]) rs.getArray("column_families").getArray()); + int segmentsRepaired = rs.getInt("segments_repaired"); + int totalSegments = rs.getInt("segments_total"); + RepairRun.RunState state = RepairRun.RunState.valueOf(rs.getString("state")); + DateTime startTime = RepairRunMapper.getDateTimeOrNull(rs, "start_time"); + DateTime endTime = RepairRunMapper.getDateTimeOrNull(rs, "end_time"); + String cause = rs.getString("cause"); + String owner = rs.getString("owner"); + String lastEvent = rs.getString("last_event"); + DateTime creationTime = RepairRunMapper.getDateTimeOrNull(rs, "creation_time"); + DateTime pauseTime = RepairRunMapper.getDateTimeOrNull(rs, "pause_time"); + Double intensity = rs.getDouble("intensity"); + Boolean incrementalRepair = rs.getBoolean("incremental_repair"); + RepairParallelism repairParallelism = RepairParallelism.fromName( + rs.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")); + + Collection nodes = ImmutableSet.copyOf((String[]) rs.getArray("nodes").getArray()); + Collection datacenters = ImmutableSet.copyOf((String[]) rs.getArray("datacenters").getArray()); + + return new RepairRunStatus( + UuidUtil.fromSequenceId(runId), + clusterName, + keyspaceName, + columnFamilies, + segmentsRepaired, + totalSegments, + state, + startTime, + endTime, + cause, + owner, + lastEvent, + creationTime, + pauseTime, + intensity, + incrementalRepair, + repairParallelism, + nodes, + datacenters); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java index 5046c3e20..072a524f1 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java @@ -11,66 +11,69 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; -import com.google.common.collect.ImmutableList; import com.spotify.reaper.core.RepairSchedule; -import org.apache.cassandra.repair.RepairParallelism; -import org.skife.jdbi.v2.StatementContext; -import org.skife.jdbi.v2.tweak.ResultSetMapper; - import java.sql.Array; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.UUID; -public class RepairScheduleMapper implements ResultSetMapper { +import com.google.common.collect.ImmutableList; +import org.apache.cassandra.repair.RepairParallelism; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultSetMapper; + +public final class RepairScheduleMapper implements ResultSetMapper { @Override - public RepairSchedule map(int index, ResultSet r, StatementContext ctx) throws SQLException { - - UUID[] runHistoryUUIDs = new UUID[0]; - + public RepairSchedule map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + + UUID[] runHistoryUuids = new UUID[0]; + Number[] runHistory = null; - Array av = r.getArray("run_history"); - if(null != av) { + Array av = rs.getArray("run_history"); + if (null != av) { Object obj = av.getArray(); - if(obj instanceof Number[]) { - runHistory = (Number[])obj; - } else if(obj instanceof Object[]) { - Object[] ol = (Object[])obj; + if (obj instanceof Number[]) { + runHistory = (Number[]) obj; + } else if (obj instanceof Object[]) { + Object[] ol = (Object[]) obj; runHistory = Arrays.copyOf(ol, ol.length, Number[].class); } - + if (null != runHistory && runHistory.length > 0) { - runHistoryUUIDs = new UUID[runHistory.length]; + runHistoryUuids = new UUID[runHistory.length]; for (int i = 0; i < runHistory.length; i++) { - runHistoryUUIDs[i] = UuidUtil.fromSequenceId(runHistory[i].longValue()); + runHistoryUuids[i] = UuidUtil.fromSequenceId(runHistory[i].longValue()); } } - } - - String stateStr = r.getString("state"); + } + + String stateStr = rs.getString("state"); // For temporary backward compatibility reasons, supporting RUNNING state as ACTIVE. if ("RUNNING".equalsIgnoreCase(stateStr)) { stateStr = "ACTIVE"; } RepairSchedule.State scheduleState = RepairSchedule.State.valueOf(stateStr); + return new RepairSchedule.Builder( - UuidUtil.fromSequenceId(r.getLong("repair_unit_id")), + UuidUtil.fromSequenceId(rs.getLong("repair_unit_id")), scheduleState, - r.getInt("days_between"), - RepairRunMapper.getDateTimeOrNull(r, "next_activation"), - ImmutableList.copyOf(runHistoryUUIDs), - r.getInt("segment_count"), - RepairParallelism.fromName(r.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")), - r.getDouble("intensity"), - RepairRunMapper.getDateTimeOrNull(r, "creation_time")) - .owner(r.getString("owner")) - .pauseTime(RepairRunMapper.getDateTimeOrNull(r, "pause_time")) - .build(UuidUtil.fromSequenceId(r.getLong("id"))); + rs.getInt("days_between"), + RepairRunMapper.getDateTimeOrNull(rs, "next_activation"), + ImmutableList.copyOf(runHistoryUuids), + rs.getInt("segment_count"), + RepairParallelism.fromName( + rs.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")), + rs.getDouble("intensity"), + RepairRunMapper.getDateTimeOrNull(rs, "creation_time")) + .owner(rs.getString("owner")) + .pauseTime(RepairRunMapper.getDateTimeOrNull(rs, "pause_time")) + .build(UuidUtil.fromSequenceId(rs.getLong("id"))); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleStatusMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleStatusMapper.java index 3d0b3c10a..35e59a5e0 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleStatusMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleStatusMapper.java @@ -11,42 +11,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; -import com.google.common.collect.ImmutableSet; import com.spotify.reaper.core.RepairSchedule; import com.spotify.reaper.resources.view.RepairScheduleStatus; +import java.sql.ResultSet; +import java.sql.SQLException; + +import com.google.common.collect.ImmutableSet; import org.apache.cassandra.repair.RepairParallelism; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class RepairScheduleStatusMapper implements ResultSetMapper { +public final class RepairScheduleStatusMapper implements ResultSetMapper { @Override - public RepairScheduleStatus map(int index, ResultSet r, StatementContext ctx) - throws SQLException { + public RepairScheduleStatus map(int index, ResultSet rs, StatementContext ctx) throws SQLException { return new RepairScheduleStatus( - UuidUtil.fromSequenceId(r.getLong("id")), - r.getString("owner"), - r.getString("cluster_name"), - r.getString("keyspace_name"), - ImmutableSet.copyOf((String[]) r.getArray("column_families").getArray()), - RepairSchedule.State.valueOf(r.getString("state")), - RepairRunMapper.getDateTimeOrNull(r, "creation_time"), - RepairRunMapper.getDateTimeOrNull(r, "next_activation"), - RepairRunMapper.getDateTimeOrNull(r, "pause_time"), - r.getDouble("intensity"), - r.getBoolean("incremental_repair"), - r.getInt("segment_count"), - RepairParallelism.fromName(r.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")), - r.getInt("days_between"), ImmutableSet.copyOf((String[]) r.getArray("nodes").getArray()), - ImmutableSet.copyOf((String[]) r.getArray("datacenters").getArray()) - ); + UuidUtil.fromSequenceId(rs.getLong("id")), + rs.getString("owner"), + rs.getString("cluster_name"), + rs.getString("keyspace_name"), + ImmutableSet.copyOf((String[]) rs.getArray("column_families").getArray()), + RepairSchedule.State.valueOf(rs.getString("state")), + RepairRunMapper.getDateTimeOrNull(rs, "creation_time"), + RepairRunMapper.getDateTimeOrNull(rs, "next_activation"), + RepairRunMapper.getDateTimeOrNull(rs, "pause_time"), + rs.getDouble("intensity"), + rs.getBoolean("incremental_repair"), + rs.getInt("segment_count"), + RepairParallelism.fromName( + rs.getString("repair_parallelism").toLowerCase().replace("datacenter_aware", "dc_parallel")), + rs.getInt("days_between"), + ImmutableSet.copyOf((String[]) rs.getArray("nodes").getArray()), + ImmutableSet.copyOf((String[]) rs.getArray("datacenters").getArray())); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairSegmentMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairSegmentMapper.java index 0dd24e4ae..4bff1cda5 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairSegmentMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairSegmentMapper.java @@ -11,30 +11,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; import com.spotify.reaper.core.RepairSegment; import com.spotify.reaper.service.RingRange; +import java.sql.ResultSet; +import java.sql.SQLException; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import java.sql.ResultSet; -import java.sql.SQLException; +public final class RepairSegmentMapper implements ResultSetMapper { -public class RepairSegmentMapper implements ResultSetMapper { + @Override + public RepairSegment map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + RingRange range + = new RingRange(rs.getBigDecimal("start_token").toBigInteger(), rs.getBigDecimal("end_token").toBigInteger()); - public RepairSegment map(int index, ResultSet r, StatementContext ctx) throws SQLException { - RingRange range = new RingRange(r.getBigDecimal("start_token").toBigInteger(), - r.getBigDecimal("end_token").toBigInteger()); - return - new RepairSegment.Builder(range, UuidUtil.fromSequenceId(r.getLong("repair_unit_id"))) - .withRunId(UuidUtil.fromSequenceId(r.getLong("run_id"))) - .state(RepairSegment.State.values()[r.getInt("state")]) - .coordinatorHost(r.getString("coordinator_host")) - .startTime(RepairRunMapper.getDateTimeOrNull(r, "start_time")) - .endTime(RepairRunMapper.getDateTimeOrNull(r, "end_time")) - .failCount(r.getInt("fail_count")) - .build(UuidUtil.fromSequenceId(r.getLong("id"))); + return new RepairSegment.Builder(range, UuidUtil.fromSequenceId(rs.getLong("repair_unit_id"))) + .withRunId(UuidUtil.fromSequenceId(rs.getLong("run_id"))) + .state(RepairSegment.State.values()[rs.getInt("state")]) + .coordinatorHost(rs.getString("coordinator_host")) + .startTime(RepairRunMapper.getDateTimeOrNull(rs, "start_time")) + .endTime(RepairRunMapper.getDateTimeOrNull(rs, "end_time")) + .failCount(rs.getInt("fail_count")) + .build(UuidUtil.fromSequenceId(rs.getLong("id"))); } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairUnitMapper.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairUnitMapper.java index 4027f98aa..ef7428be2 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairUnitMapper.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RepairUnitMapper.java @@ -11,32 +11,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; + +import com.spotify.reaper.core.RepairUnit; + import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; +import com.google.common.collect.Sets; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; -import com.google.common.collect.Sets; -import com.spotify.reaper.core.RepairUnit; - -public class RepairUnitMapper implements ResultSetMapper { +public final class RepairUnitMapper implements ResultSetMapper { @Override - public RepairUnit map(int index, ResultSet r, StatementContext ctx) throws SQLException { - - String[] columnFamilies = parseStringArray(r.getArray("column_families").getArray()); - String[] nodes = parseStringArray(r.getArray("nodes").getArray()); - String[] datacenters = parseStringArray(r.getArray("datacenters").getArray()); - - - RepairUnit.Builder builder = new RepairUnit.Builder(r.getString("cluster_name"), r.getString("keyspace_name"), - Sets.newHashSet(columnFamilies), r.getBoolean("incremental_repair"), Sets.newHashSet(nodes), + public RepairUnit map(int index, ResultSet rs, StatementContext ctx) throws SQLException { + + String[] columnFamilies = parseStringArray(rs.getArray("column_families").getArray()); + String[] nodes = parseStringArray(rs.getArray("nodes").getArray()); + String[] datacenters = parseStringArray(rs.getArray("datacenters").getArray()); + + RepairUnit.Builder builder = new RepairUnit.Builder( + rs.getString("cluster_name"), + rs.getString("keyspace_name"), + Sets.newHashSet(columnFamilies), + rs.getBoolean("incremental_repair"), + Sets.newHashSet(nodes), Sets.newHashSet(datacenters)); - return builder.build(UuidUtil.fromSequenceId(r.getLong("id"))); + + return builder.build(UuidUtil.fromSequenceId(rs.getLong("id"))); } private String[] parseStringArray(Object obj) { diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RunStateArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RunStateArgumentFactory.java index ce889a4e0..f70fb5273 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RunStateArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/RunStateArgumentFactory.java @@ -11,21 +11,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.storage.postgresql; import com.spotify.reaper.core.RepairRun; +import java.sql.PreparedStatement; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; -import java.sql.PreparedStatement; -import java.sql.SQLException; - /** * Provides JDBI a method to map RunState value to a TEXT value in database. */ -public class RunStateArgumentFactory implements ArgumentFactory { +public final class RunStateArgumentFactory implements ArgumentFactory { @Override public boolean accepts(Class expectedType, Object value, StatementContext ctx) { @@ -33,13 +33,9 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx } @Override - public Argument build(Class expectedType, final RepairRun.RunState value, - StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - statement.setString(position, value.toString()); - } + public Argument build(Class expectedType, final RepairRun.RunState value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + statement.setString(position, value.toString()); }; } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ScheduleStateArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ScheduleStateArgumentFactory.java index fcf5f4d7f..34630bd2b 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ScheduleStateArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/ScheduleStateArgumentFactory.java @@ -1,15 +1,28 @@ +/* + * 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.spotify.reaper.storage.postgresql; import com.spotify.reaper.core.RepairSchedule; +import java.sql.PreparedStatement; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -public class ScheduleStateArgumentFactory implements ArgumentFactory { +public final class ScheduleStateArgumentFactory implements ArgumentFactory { @Override public boolean accepts(Class expectedType, Object value, StatementContext ctx) { @@ -17,13 +30,9 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx } @Override - public Argument build(Class expectedType, final RepairSchedule.State value, - StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - statement.setString(position, value.toString()); - } + public Argument build(Class expectedType, final RepairSchedule.State value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + statement.setString(position, value.toString()); }; } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/StateArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/StateArgumentFactory.java index 81db39b86..39b59353d 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/StateArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/StateArgumentFactory.java @@ -1,18 +1,31 @@ +/* + * 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.spotify.reaper.storage.postgresql; import com.spotify.reaper.core.RepairSegment; +import java.sql.PreparedStatement; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; -import java.sql.PreparedStatement; -import java.sql.SQLException; - /** * Provides JDBI a method to map State value to an INT value in database. */ -public class StateArgumentFactory implements ArgumentFactory { +public final class StateArgumentFactory implements ArgumentFactory { @Override public boolean accepts(Class expectedType, Object value, StatementContext ctx) { @@ -20,13 +33,9 @@ public boolean accepts(Class expectedType, Object value, StatementContext ctx } @Override - public Argument build(Class expectedType, final RepairSegment.State value, - StatementContext ctx) { - return new Argument() { - public void apply(int position, PreparedStatement statement, StatementContext ctx) - throws SQLException { - statement.setInt(position, value.ordinal()); - } + public Argument build(Class expectedType, final RepairSegment.State value, StatementContext ctx) { + return (int position, PreparedStatement statement, StatementContext ctx1) -> { + statement.setInt(position, value.ordinal()); }; } } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidArgumentFactory.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidArgumentFactory.java index 80bb5616f..0a6eb97bb 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidArgumentFactory.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidArgumentFactory.java @@ -1,11 +1,26 @@ +/* + * 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.spotify.reaper.storage.postgresql; + +import java.util.UUID; + import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.Argument; import org.skife.jdbi.v2.tweak.ArgumentFactory; -import java.util.UUID; - /** * Provides JDBI a method to map UUID value to a long (most sig bits) value in database. */ diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidUtil.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidUtil.java index 8c0fee63f..5333073b9 100644 --- a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidUtil.java +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/UuidUtil.java @@ -1,10 +1,26 @@ +/* + * 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.spotify.reaper.storage.postgresql; import java.util.UUID; -import com.datastax.driver.core.utils.UUIDs; - public final class UuidUtil { + + private UuidUtil() { + } + public static UUID fromSequenceId(long insertedId) { return new UUID(insertedId, 0L); } @@ -13,5 +29,4 @@ public static long toSequenceId(UUID id) { return id.getMostSignificantBits(); } - private UuidUtil() {} } diff --git a/src/server/src/main/java/com/spotify/reaper/storage/postgresql/package-info.java b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/package-info.java new file mode 100644 index 000000000..a9d340928 --- /dev/null +++ b/src/server/src/main/java/com/spotify/reaper/storage/postgresql/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +@javax.annotation.ParametersAreNonnullByDefault +package com.spotify.reaper.storage.postgresql; diff --git a/src/server/src/test/java/com/spotify/reaper/AssertionTest.java b/src/server/src/test/java/com/spotify/reaper/AssertionTest.java index 9dd5a36d8..3e89f1451 100644 --- a/src/server/src/test/java/com/spotify/reaper/AssertionTest.java +++ b/src/server/src/test/java/com/spotify/reaper/AssertionTest.java @@ -1,22 +1,34 @@ +/* + * 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.spotify.reaper; import org.junit.Test; +public final class AssertionTest { -public final class AssertionTest{ - - @Test - public void test_assertions_enabled(){ - boolean asserted = false; - try{ - assert false; - }catch (AssertionError error){ - asserted = true; - } - if (!asserted){ - throw new AssertionError("assertions are not enabled"); - } + @Test + public void test_assertions_enabled() { + boolean asserted = false; + try { + assert false; + } catch (AssertionError error) { + asserted = true; + } + if (!asserted) { + throw new AssertionError("assertions are not enabled"); } + } -} \ No newline at end of file +} diff --git a/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationBuilder.java b/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationBuilder.java index a38dce699..bcfb4ed76 100644 --- a/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationBuilder.java +++ b/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationBuilder.java @@ -1,11 +1,26 @@ +/* + * 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.spotify.reaper; -import org.apache.cassandra.repair.RepairParallelism; import java.time.Duration; import java.util.Map; -public class ReaperApplicationConfigurationBuilder { +import org.apache.cassandra.repair.RepairParallelism; + +public final class ReaperApplicationConfigurationBuilder { private Integer segmentCount; private RepairParallelism repairParallelism; @@ -76,7 +91,9 @@ public ReaperApplicationConfigurationBuilder withJmxAuth(ReaperApplicationConfig return this; } - public ReaperApplicationConfigurationBuilder withAutoScheduling(ReaperApplicationConfiguration.AutoSchedulingConfiguration autoRepairScheduling) { + public ReaperApplicationConfigurationBuilder withAutoScheduling( + ReaperApplicationConfiguration.AutoSchedulingConfiguration autoRepairScheduling) { + this.autoRepairScheduling = autoRepairScheduling; return this; } @@ -98,6 +115,7 @@ public ReaperApplicationConfiguration build() { } public static class AutoSchedulingConfigurationBuilder { + private Boolean enabled; private Duration initialDelayPeriod; private Duration periodBetweenPolls; @@ -105,7 +123,9 @@ public static class AutoSchedulingConfigurationBuilder { private Duration scheduleSpreadPeriod; public ReaperApplicationConfiguration.AutoSchedulingConfiguration build() { - ReaperApplicationConfiguration.AutoSchedulingConfiguration autoSchedulingConfig = new ReaperApplicationConfiguration.AutoSchedulingConfiguration(); + ReaperApplicationConfiguration.AutoSchedulingConfiguration autoSchedulingConfig + = new ReaperApplicationConfiguration.AutoSchedulingConfiguration(); + autoSchedulingConfig.setEnabled(enabled); autoSchedulingConfig.setInitialDelayPeriod(initialDelayPeriod); autoSchedulingConfig.setPeriodBetweenPolls(periodBetweenPolls); diff --git a/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationTest.java b/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationTest.java index ae362a096..092f7beab 100644 --- a/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationTest.java +++ b/src/server/src/test/java/com/spotify/reaper/ReaperApplicationConfigurationTest.java @@ -1,19 +1,33 @@ +/* + * 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.spotify.reaper; -import org.apache.cassandra.repair.RepairParallelism; -import org.hibernate.validator.HibernateValidator; -import org.junit.Before; -import org.junit.Test; import javax.validation.Validation; import javax.validation.Validator; import io.dropwizard.db.DataSourceFactory; +import org.apache.cassandra.repair.RepairParallelism; +import org.hibernate.validator.HibernateValidator; +import org.junit.Before; +import org.junit.Test; import systems.composable.dropwizard.cassandra.CassandraFactory; import static org.fest.assertions.api.Assertions.assertThat; -public class ReaperApplicationConfigurationTest { +public final class ReaperApplicationConfigurationTest { private final Validator validator = Validation .byProvider(HibernateValidator.class) @@ -54,4 +68,4 @@ public void testRepairIntensity() { config.setRepairIntensity(1); assertThat(validator.validate(config)).hasSize(0); } -} \ No newline at end of file +} diff --git a/src/server/src/test/java/com/spotify/reaper/SimpleReaperClient.java b/src/server/src/test/java/com/spotify/reaper/SimpleReaperClient.java index 1e313f036..5a2514f1c 100644 --- a/src/server/src/test/java/com/spotify/reaper/SimpleReaperClient.java +++ b/src/server/src/test/java/com/spotify/reaper/SimpleReaperClient.java @@ -1,23 +1,29 @@ -package com.spotify.reaper; +/* + * 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. + */ -import com.google.common.base.Optional; +package com.spotify.reaper; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.spotify.reaper.resources.view.RepairRunStatus; import com.spotify.reaper.resources.view.RepairScheduleStatus; + import java.io.IOException; import java.net.MalformedURLException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.List; import java.util.Map; - import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; @@ -26,6 +32,12 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import static org.junit.Assert.assertEquals; /** @@ -35,11 +47,23 @@ public final class SimpleReaperClient { private static final Logger LOG = LoggerFactory.getLogger(SimpleReaperClient.class); - private static final Optional> EMPTY_PARAMS = Optional.absent(); - public static Response doHttpCall(String httpMethod, String host, int port, String urlPath, - Optional> params) { + private final String reaperHost; + private final int reaperPort; + + public SimpleReaperClient(String reaperHost, int reaperPort) { + this.reaperHost = reaperHost; + this.reaperPort = reaperPort; + } + + public static Response doHttpCall( + String httpMethod, + String host, + int port, + String urlPath, + Optional> params) { + String reaperBase = "http://" + host.toLowerCase() + ":" + port + "/"; URI uri; try { @@ -47,26 +71,28 @@ public static Response doHttpCall(String httpMethod, String host, int port, Stri } catch (MalformedURLException | URISyntaxException ex) { throw new RuntimeException(ex); } - + Client client = ClientBuilder.newClient(); WebTarget webTarget = client.target(uri); - + LOG.info("calling (" + httpMethod + ") Reaper in resource: " + webTarget.getUri()); if (params.isPresent()) { for (Map.Entry entry : params.get().entrySet()) { webTarget = webTarget.queryParam(entry.getKey(), entry.getValue()); } } - + Invocation.Builder invocationBuilder = webTarget.request(MediaType.APPLICATION_JSON); - + Response response; if ("GET".equalsIgnoreCase(httpMethod)) { response = invocationBuilder.get(); + } else if ("HEAD".equalsIgnoreCase(httpMethod)) { + response = invocationBuilder.head(); } else if ("POST".equalsIgnoreCase(httpMethod)) { response = invocationBuilder.post(null); } else if ("PUT".equalsIgnoreCase(httpMethod)) { - response = invocationBuilder.put(Entity.entity("",MediaType.APPLICATION_JSON)); + response = invocationBuilder.put(Entity.entity("", MediaType.APPLICATION_JSON)); } else if ("DELETE".equalsIgnoreCase(httpMethod)) { response = invocationBuilder.delete(); } else if ("OPTIONS".equalsIgnoreCase(httpMethod)) { @@ -75,7 +101,7 @@ public static Response doHttpCall(String httpMethod, String host, int port, Stri throw new RuntimeException("Invalid HTTP method: " + httpMethod); } - return response; + return response; } private static T parseJSON(String json, TypeReference ref) { @@ -88,43 +114,41 @@ private static T parseJSON(String json, TypeReference ref) { } public static List parseRepairScheduleStatusListJSON(String json) { - return parseJSON(json, new TypeReference>() {}); + return parseJSON(json, new TypeReference>() { + }); } public static RepairScheduleStatus parseRepairScheduleStatusJSON(String json) { - return parseJSON(json, new TypeReference() {}); + return parseJSON(json, new TypeReference() { + }); } public static List parseRepairRunStatusListJSON(String json) { - return parseJSON(json, new TypeReference>() {}); + return parseJSON(json, new TypeReference>() { + }); } public static RepairRunStatus parseRepairRunStatusJSON(String json) { - return parseJSON(json, new TypeReference() {}); + return parseJSON(json, new TypeReference() { + }); } - + public static Map parseClusterStatusJSON(String json) { - return parseJSON(json, new TypeReference >() {}); - } - - public static List parseClusterNameListJSON(String json) { - return parseJSON(json, new TypeReference >() {}); + return parseJSON(json, new TypeReference>() { + }); } - private final String reaperHost; - private final int reaperPort; - - public SimpleReaperClient(String reaperHost, int reaperPort) { - this.reaperHost = reaperHost; - this.reaperPort = reaperPort; + public static List parseClusterNameListJSON(String json) { + return parseJSON(json, new TypeReference>() { + }); } public List getRepairSchedulesForCluster(String clusterName) { Response response = doHttpCall("GET", reaperHost, reaperPort, - "/repair_schedule/cluster/" + clusterName, EMPTY_PARAMS); + "/repair_schedule/cluster/" + clusterName, EMPTY_PARAMS); assertEquals(200, response.getStatus()); String responseData = response.readEntity(String.class); - return parseRepairScheduleStatusListJSON(responseData); + return parseRepairScheduleStatusListJSON(responseData); } } diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/AddClusterTest.java b/src/server/src/test/java/com/spotify/reaper/acceptance/AddClusterTest.java index d4a0042dd..0c3301965 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/AddClusterTest.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/AddClusterTest.java @@ -11,17 +11,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.acceptance; -import org.junit.runner.RunWith; import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; @RunWith(Cucumber.class) @CucumberOptions( features = "classpath:com.spotify.reaper.acceptance/basic_reaper_functionality.feature" -) -public class AddClusterTest { + ) +public final class AddClusterTest { // Required only to get the Cucumber acceptance tests actually run. } diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/BasicSteps.java b/src/server/src/test/java/com/spotify/reaper/acceptance/BasicSteps.java index 8ad18f714..449c9923f 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/BasicSteps.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/BasicSteps.java @@ -1,13 +1,29 @@ +/* + * 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.spotify.reaper.acceptance; -import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.awaitility.Awaitility.await; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.SimpleReaperClient; +import com.spotify.reaper.cassandra.JmxConnectionFactory; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.cassandra.RepairStatusHandler; +import com.spotify.reaper.resources.CommonTools; +import com.spotify.reaper.resources.view.RepairRunStatus; +import com.spotify.reaper.resources.view.RepairScheduleStatus; +import com.spotify.reaper.storage.CassandraStorage; import java.math.BigInteger; import java.util.Arrays; @@ -17,15 +33,8 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; - import javax.ws.rs.core.Response; -import org.assertj.core.api.Assertions; -import org.joda.time.DateTime; -import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Host; import com.datastax.driver.core.Session; @@ -36,20 +45,23 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.SimpleReaperClient; -import com.spotify.reaper.cassandra.JmxConnectionFactory; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.resources.CommonTools; -import com.spotify.reaper.resources.view.RepairRunStatus; -import com.spotify.reaper.resources.view.RepairScheduleStatus; -import com.spotify.reaper.storage.CassandraStorage; - import cucumber.api.java.Before; import cucumber.api.java.en.And; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; +import org.assertj.core.api.Assertions; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Basic acceptance test (Cucumber) steps. @@ -58,27 +70,27 @@ public final class BasicSteps { private static final Logger LOG = LoggerFactory.getLogger(BasicSteps.class); private static final Optional> EMPTY_PARAMS = Optional.absent(); - private static final String MEMORY_CONFIG_FILE="cassandra-reaper-at.yaml"; + private static final String MEMORY_CONFIG_FILE = "cassandra-reaper-at.yaml"; private static final List RUNNERS = new CopyOnWriteArrayList<>(); private static final List CLIENTS = new CopyOnWriteArrayList<>(); private static final Random RAND = new Random(System.nanoTime()); - public synchronized static void addReaperRunner(ReaperTestJettyRunner runner) { + public static synchronized void addReaperRunner(ReaperTestJettyRunner runner) { if (!CLIENTS.isEmpty()) { - Preconditions.checkState(runner.runnerInstance.context.storage instanceof CassandraStorage); + Preconditions.checkState(runner.runnerInstance.context.storage instanceof CassandraStorage); - RUNNERS.stream() - .forEach(r -> Preconditions.checkState(r.runnerInstance.context.storage instanceof CassandraStorage)); + RUNNERS.stream() + .forEach(r -> Preconditions.checkState(r.runnerInstance.context.storage instanceof CassandraStorage)); } RUNNERS.add(runner); CLIENTS.add(runner.getClient()); } - public synchronized static void removeReaperRunner(ReaperTestJettyRunner runner) { - CLIENTS.remove(runner.getClient()); - RUNNERS.remove(runner); + public static synchronized void removeReaperRunner(ReaperTestJettyRunner runner) { + CLIENTS.remove(runner.getClient()); + RUNNERS.remove(runner); } @Before @@ -87,17 +99,20 @@ public static void setup() { } private void setupReaperTestRunner() throws Exception { - if(CLIENTS.isEmpty()) { + if (CLIENTS.isEmpty()) { assert RUNNERS.isEmpty(); - LOG.info("setting up testing Reaper runner with {} seed hosts defined", + LOG.info( + "setting up testing Reaper runner with {} seed hosts defined", TestContext.TEST_CLUSTER_SEED_HOSTS.size()); + AppContext context = new AppContext(); - context.jmxConnectionFactory = mock(JmxConnectionFactory.class); + final Map proxies = Maps.newConcurrentMap(); + for (String seedHost : TestContext.TEST_CLUSTER_SEED_HOSTS.keySet()) { String clusterName = TestContext.TEST_CLUSTER_SEED_HOSTS.get(seedHost); - Map> clusterKeyspaces = TestContext.TEST_CLUSTER_INFO.get(clusterName); Map hosts = Maps.newHashMap(); hosts.put(seedHost, seedHost); + Map> clusterKeyspaces = TestContext.TEST_CLUSTER_INFO.get(clusterName); JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn(clusterName); when(jmx.getPartitioner()).thenReturn("org.apache.cassandra.dht.RandomPartitioner"); @@ -109,9 +124,17 @@ private void setupReaperTestRunner() throws Exception { for (String keyspace : clusterKeyspaces.keySet()) { when(jmx.getTableNamesForKeyspace(keyspace)).thenReturn(clusterKeyspaces.get(keyspace)); } - when(context.jmxConnectionFactory.connect(org.mockito.Matchers.any(), eq(seedHost), Mockito.anyInt())) - .thenReturn(jmx); + proxies.put(seedHost, jmx); } + + context.jmxConnectionFactory = new JmxConnectionFactory() { + @Override + public JmxProxy connect(Optional handler, String host, int connectionTimeout) + throws ReaperException { + + return proxies.get(host); + } + }; ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); runner.setup(context, MEMORY_CONFIG_FILE); addReaperRunner(runner); @@ -119,66 +142,76 @@ private void setupReaperTestRunner() throws Exception { } private void setupReaperIntegrationTestRunner() throws Exception { - if(CLIENTS.isEmpty()) { - assert RUNNERS.isEmpty(); - LOG.info("setting up testing Reaper runner with {} seed hosts defined", - TestContext.TEST_CLUSTER_SEED_HOSTS.size()); - AppContext context = new AppContext(); - ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); - runner.setup(context, MEMORY_CONFIG_FILE); - addReaperRunner(runner); + if (CLIENTS.isEmpty()) { + assert RUNNERS.isEmpty(); + LOG.info("setting up testing Reaper runner with {} seed hosts defined", + TestContext.TEST_CLUSTER_SEED_HOSTS.size()); + AppContext context = new AppContext(); + ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); + runner.setup(context, MEMORY_CONFIG_FILE); + addReaperRunner(runner); } } private static void callAndExpect( - String httpMethod, - String callPath, - Optional> params, - Optional expectedDataInResponseData, - Response.Status... expectedStatuses) { + String httpMethod, + String callPath, + Optional> params, + Optional expectedDataInResponseData, + Response.Status... expectedStatuses) { + + RUNNERS.parallelStream().forEach(runner -> { + Response response = runner.callReaper(httpMethod, callPath, params); + + Assertions + .assertThat(Arrays.asList(expectedStatuses).stream().map(Response.Status::getStatusCode)) + .contains(response.getStatus()); + + if (1 == RUNNERS.size() && expectedStatuses[0].getStatusCode() != response.getStatus()) { + // we can't fail on this because the jersey client sometimes sends + // duplicate http requests and the wrong request responds firs + LOG.error( + "AssertionError: expected: {} but was: {}", + expectedStatuses[0].getStatusCode(), response.getStatus()); + } - RUNNERS.parallelStream().forEach(runner -> { - Response response = runner.callReaper(httpMethod, callPath, params); - - Assertions - .assertThat(Arrays.asList(expectedStatuses).stream().map(Response.Status::getStatusCode)) - .contains(response.getStatus()); - - if (1 == RUNNERS.size() && expectedStatuses[0].getStatusCode() != response.getStatus()) { - // we can't fail on this because the jersey client sometimes sends - // duplicate http requests and the wrong request responds first - LOG.error( - "AssertionError: expected: {} but was: {}", - expectedStatuses[0].getStatusCode(), response.getStatus()); - } - - if (expectedStatuses[0].getStatusCode() == response.getStatus()) { - if (expectedDataInResponseData.isPresent()) { - String responseEntity = response.readEntity(String.class); - assertTrue( - "expected data not found from the response: " + expectedDataInResponseData.get(), - 0 != responseEntity.length() && responseEntity.contains(expectedDataInResponseData.get())); - - LOG.debug("Data \"" + expectedDataInResponseData.get() + "\" was found from response data"); - } - } - }); + if (expectedStatuses[0].getStatusCode() == response.getStatus()) { + if (expectedDataInResponseData.isPresent()) { + String responseEntity = response.readEntity(String.class); + assertTrue( + "expected data not found from the response: " + expectedDataInResponseData.get(), + 0 != responseEntity.length() && responseEntity.contains(expectedDataInResponseData.get())); + + LOG.debug("Data \"" + expectedDataInResponseData.get() + "\" was found from response data"); + } + } + }); } @Given("^a reaper service is running$") public void a_reaper_service_is_running() throws Throwable { synchronized (BasicSteps.class) { - setupReaperTestRunner(); - //callAndExpect("GET", "/ping", Optional.>absent(), - // Response.Status.OK, Optional.absent()); + setupReaperTestRunner(); + +// callAndExpect( +// "HEAD", +// "/ping", +// Optional.>absent(), +// Optional.absent(), +// Response.Status.NO_CONTENT); } } @Given("^a real reaper service is running$") public void a_real_reaper_service_is_running() throws Throwable { synchronized (BasicSteps.class) { - setupReaperIntegrationTestRunner(); - callAndExpect("GET", "/ping", Optional.>absent(), Optional.absent(), Response.Status.OK); + setupReaperIntegrationTestRunner(); + callAndExpect( + "GET", + "/ping", + Optional.>absent(), + Optional.absent(), + Response.Status.OK); } } @@ -191,39 +224,37 @@ public void cluster_seed_host_points_to_cluster_with_name(String seedHost, Strin @Given("^cluster \"([^\"]*)\" has keyspace \"([^\"]*)\" with tables \"([^\"]*)\"$") public void cluster_has_keyspace_with_tables(String clusterName, String keyspace, - String tablesListStr) throws Throwable { + String tablesListStr) throws Throwable { synchronized (BasicSteps.class) { - Set tables = - Sets.newHashSet(CommonTools.COMMA_SEPARATED_LIST_SPLITTER.split(tablesListStr)); - TestContext.addClusterInfo(clusterName, keyspace, tables); + Set tables = Sets.newHashSet(CommonTools.COMMA_SEPARATED_LIST_SPLITTER.split(tablesListStr)); + TestContext.addClusterInfo(clusterName, keyspace, tables); } } @Given("^ccm cluster \"([^\"]*)\" has keyspace \"([^\"]*)\" with tables \"([^\"]*)\"$") public void ccm_cluster_has_keyspace_with_tables(String clusterName, String keyspace, - String tablesListStr) throws Throwable { + String tablesListStr) throws Throwable { synchronized (BasicSteps.class) { - Set tables = - Sets.newHashSet(CommonTools.COMMA_SEPARATED_LIST_SPLITTER.split(tablesListStr)); - createKeyspace(keyspace); - tables.stream().forEach(tableName -> createTable(keyspace, tableName)); - TestContext.addClusterInfo(clusterName, keyspace, tables); + Set tables = Sets.newHashSet(CommonTools.COMMA_SEPARATED_LIST_SPLITTER.split(tablesListStr)); + createKeyspace(keyspace); + tables.stream().forEach(tableName -> createTable(keyspace, tableName)); + TestContext.addClusterInfo(clusterName, keyspace, tables); } } @Given("^that we are going to use \"([^\"]*)\" as cluster seed host$") public void that_we_are_going_to_use_as_cluster_seed_host(String seedHost) throws Throwable { synchronized (BasicSteps.class) { - TestContext.SEED_HOST = seedHost; + TestContext.SEED_HOST = seedHost; } } @And("^reaper has no cluster with name \"([^\"]*)\" in storage$") public void reaper_has_no_cluster_with_name_in_storage(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect("GET", "/cluster/" + clusterName, - Optional.>absent(), - Optional.absent(), Response.Status.NOT_FOUND); + callAndExpect("GET", "/cluster/" + clusterName, + Optional.>absent(), + Optional.absent(), Response.Status.NOT_FOUND); } } @@ -231,7 +262,7 @@ public void reaper_has_no_cluster_with_name_in_storage(String clusterName) throw public void reaper_has_no_cluster_in_storage() throws Throwable { synchronized (BasicSteps.class) { RUNNERS.parallelStream().forEach(runner -> { - Response response = runner.callReaper("GET", "/cluster/", Optional.>absent()); + Response response = runner.callReaper("GET", "/cluster/", Optional.>absent()); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); String responseData = response.readEntity(String.class); List clusterNames = SimpleReaperClient.parseClusterNameListJSON(responseData); @@ -243,159 +274,164 @@ public void reaper_has_no_cluster_in_storage() throws Throwable { @When("^an add-cluster request is made to reaper$") public void an_add_cluster_request_is_made_to_reaper() throws Throwable { synchronized (BasicSteps.class) { - final StringBuffer responseData = new StringBuffer(); + final StringBuffer responseData = new StringBuffer(); - RUNNERS.parallelStream().forEach(runner -> { - Map params = Maps.newHashMap(); - params.put("seedHost", TestContext.SEED_HOST); - Response response = runner.callReaper("POST", "/cluster", Optional.of(params)); + RUNNERS.parallelStream().forEach(runner -> { + Map params = Maps.newHashMap(); + params.put("seedHost", TestContext.SEED_HOST); + Response response = runner.callReaper("POST", "/cluster", Optional.of(params)); - Assertions.assertThat(ImmutableList.of( - Response.Status.CREATED.getStatusCode(), - Response.Status.FORBIDDEN.getStatusCode())) - .contains(response.getStatus()); + Assertions.assertThat(ImmutableList.of( + Response.Status.CREATED.getStatusCode(), + Response.Status.FORBIDDEN.getStatusCode())) + .contains(response.getStatus()); - if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { - responseData.append(response.readEntity(String.class)); - Map cluster = SimpleReaperClient.parseClusterStatusJSON(responseData.toString()); - TestContext.TEST_CLUSTER = (String) cluster.get("name"); - } - }); + if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { + responseData.append(response.readEntity(String.class)); + Map cluster = SimpleReaperClient.parseClusterStatusJSON(responseData.toString()); + TestContext.TEST_CLUSTER = (String) cluster.get("name"); + } + }); - Assertions.assertThat(responseData).isNotEmpty(); + Assertions.assertThat(responseData).isNotEmpty(); - callAndExpect("GET", "/cluster/" + TestContext.TEST_CLUSTER, - Optional.>absent(), - Optional.absent(), - Response.Status.OK); + callAndExpect("GET", "/cluster/" + TestContext.TEST_CLUSTER, + Optional.>absent(), + Optional.absent(), + Response.Status.OK); } } @Then("^reaper has a cluster called \"([^\"]*)\" in storage$") public void reaper_has_a_cluster_called_in_storage(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect("GET", "/cluster/" + clusterName, - Optional.>absent(), - Optional.absent(), Response.Status.OK); + callAndExpect("GET", "/cluster/" + clusterName, + Optional.>absent(), + Optional.absent(), Response.Status.OK); } } @Then("^reaper has the last added cluster in storage$") public void reaper_has_the_last_added_cluster_in_storage() throws Throwable { synchronized (BasicSteps.class) { - callAndExpect("GET", "/cluster/" + TestContext.TEST_CLUSTER, - Optional.>absent(), - Optional.absent(), Response.Status.OK); + callAndExpect("GET", "/cluster/" + TestContext.TEST_CLUSTER, + Optional.>absent(), + Optional.absent(), Response.Status.OK); } } @And("^reaper has no scheduled repairs for \"([^\"]*)\"$") public void reaper_has_no_scheduled_repairs_for(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect("GET", "/repair_schedule/cluster/" + clusterName, - Optional.>absent(), - Optional.of("[]"), Response.Status.OK); + callAndExpect("GET", "/repair_schedule/cluster/" + clusterName, + Optional.>absent(), + Optional.of("[]"), Response.Status.OK); } } @When("^a new daily \"([^\"]*)\" repair schedule is added for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") - public void a_new_daily_repair_schedule_is_added_for(String repairType, String clusterName, String keyspace) throws Throwable { - synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", clusterName); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("intensity", "0.9"); - params.put("scheduleDaysBetween", "1"); - params.put("repairParallelism", repairType.equals("incremental") ? "parallel" : "sequential"); - params.put("incrementalRepair", repairType.equals("incremental") ? "True" : "False"); - Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); - - Assertions.assertThat(ImmutableList.of( - Response.Status.CREATED.getStatusCode(), - Response.Status.CONFLICT.getStatusCode())) - .contains(response.getStatus()); + public void a_new_daily_repair_schedule_is_added_for(String repairType, String clusterName, String keyspace) + throws Throwable { - if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { - String responseData = response.readEntity(String.class); - RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = schedule.getId(); - } else { - response = runner.callReaper("GET", "/repair_schedule/cluster/" + clusterName, EMPTY_PARAMS); - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); - Assertions.assertThat(schedules).hasSize(1); - TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); - } + synchronized (BasicSteps.class) { + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", clusterName); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("intensity", "0.9"); + params.put("scheduleDaysBetween", "1"); + params.put("repairParallelism", repairType.equals("incremental") ? "parallel" : "sequential"); + params.put("incrementalRepair", repairType.equals("incremental") ? "True" : "False"); + Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); + + Assertions.assertThat(ImmutableList.of( + Response.Status.CREATED.getStatusCode(), + Response.Status.CONFLICT.getStatusCode())) + .contains(response.getStatus()); + + if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { + String responseData = response.readEntity(String.class); + RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = schedule.getId(); + } else { + response = runner.callReaper("GET", "/repair_schedule/cluster/" + clusterName, EMPTY_PARAMS); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); + Assertions.assertThat(schedules).hasSize(1); + TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); + } } } @When("^a new daily \"([^\"]*)\" repair schedule is added for the last added cluster and keyspace \"([^\"]*)\"$") - public void a_new_daily_repair_schedule_is_added_for_the_last_added_cluster(String repairType, String keyspace) throws Throwable { - synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", TestContext.TEST_CLUSTER); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("intensity", "0.9"); - params.put("scheduleDaysBetween", "1"); - params.put("repairParallelism", repairType.equals("incremental") ? "parallel" : "sequential"); - params.put("incrementalRepair", repairType.equals("incremental") ? "True" : "False"); - Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); - - Assertions.assertThat(ImmutableList.of( - Response.Status.CREATED.getStatusCode(), - Response.Status.CONFLICT.getStatusCode())) - .contains(response.getStatus()); + public void a_new_daily_repair_schedule_is_added_for_the_last_added_cluster(String repairType, String keyspace) + throws Throwable { - if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { - String responseData = response.readEntity(String.class); - RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = schedule.getId(); - } else { - response = runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); - Assertions.assertThat(schedules).hasSize(1); - TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); - } + synchronized (BasicSteps.class) { + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", TestContext.TEST_CLUSTER); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("intensity", "0.9"); + params.put("scheduleDaysBetween", "1"); + params.put("repairParallelism", repairType.equals("incremental") ? "parallel" : "sequential"); + params.put("incrementalRepair", repairType.equals("incremental") ? "True" : "False"); + Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); + + Assertions.assertThat(ImmutableList.of( + Response.Status.CREATED.getStatusCode(), + Response.Status.CONFLICT.getStatusCode())) + .contains(response.getStatus()); + + if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { + String responseData = response.readEntity(String.class); + RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = schedule.getId(); + } else { + response = runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); + Assertions.assertThat(schedules).hasSize(1); + TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); + } } } - @When("^a new daily repair schedule is added for the last added cluster and keyspace \"([^\"]*)\" with next repair immediately$") + @When("^a new daily repair schedule is added for the last added cluster " + + "and keyspace \"([^\"]*)\" with next repair immediately$") public void a_new_daily_repair_schedule_is_added_for_the_last_added_cluster_and_keyspace_with_next_repair_immediately( - String keyspace) - throws Throwable { - synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - LOG.info("adding a new daily repair schedule to keyspace: {}", keyspace); - Map params = Maps.newHashMap(); - params.put("clusterName", TestContext.TEST_CLUSTER); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("intensity", "0.9"); - params.put("scheduleDaysBetween", "1"); - params.put("scheduleTriggerTime", DateTime.now().plusSeconds(1).toString()); - Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); + String keyspace) throws Throwable { - Assertions.assertThat(ImmutableList.of( - Response.Status.CREATED.getStatusCode(), - Response.Status.CONFLICT.getStatusCode())) - .contains(response.getStatus()); - - if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { - String responseData = response.readEntity(String.class); - RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = schedule.getId(); - } else { - response = runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); - Assertions.assertThat(schedules).hasSize(1); - TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); - } + synchronized (BasicSteps.class) { + LOG.info("adding a new daily repair schedule to keyspace: {}", keyspace); + Map params = Maps.newHashMap(); + params.put("clusterName", TestContext.TEST_CLUSTER); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("intensity", "0.9"); + params.put("scheduleDaysBetween", "1"); + params.put("scheduleTriggerTime", DateTime.now().plusSeconds(1).toString()); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); + + Assertions.assertThat(ImmutableList.of( + Response.Status.CREATED.getStatusCode(), + Response.Status.CONFLICT.getStatusCode())) + .contains(response.getStatus()); + + if (Response.Status.CREATED.getStatusCode() == response.getStatus()) { + String responseData = response.readEntity(String.class); + RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = schedule.getId(); + } else { + response = runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + List schedules = SimpleReaperClient.parseRepairScheduleStatusListJSON(keyspace); + Assertions.assertThat(schedules).hasSize(1); + TestContext.LAST_MODIFIED_ID = schedules.get(0).getId(); + } } } @@ -404,18 +440,17 @@ public void a_scheduled_repair_run_has_started_for_cluster(String clusterName) t synchronized (BasicSteps.class) { RUNNERS.parallelStream().forEach(runner -> { LOG.info("waiting for a scheduled repair run to start for cluster: {}", clusterName); - await().with().pollInterval(10, SECONDS).atMost(2, MINUTES).until(() -> - { - Response response = - runner.callReaper("GET", "/repair_run/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); - - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - List runs = SimpleReaperClient.parseRepairRunStatusListJSON(responseData); - if (!runs.isEmpty()) { - TestContext.LAST_MODIFIED_ID = runs.get(0).getId(); - } - return !runs.isEmpty(); + await().with().pollInterval(10, SECONDS).atMost(2, MINUTES).until(() -> { + Response response = runner + .callReaper("GET", "/repair_run/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + List runs = SimpleReaperClient.parseRepairRunStatusListJSON(responseData); + if (!runs.isEmpty()) { + TestContext.LAST_MODIFIED_ID = runs.get(0).getId(); + } + return !runs.isEmpty(); }); }); } @@ -424,29 +459,33 @@ public void a_scheduled_repair_run_has_started_for_cluster(String clusterName) t @And("^reaper has scheduled repair for cluster called \"([^\"]*)\"$") public void reaper_has_scheduled_repair_for_cluster_called(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect("GET", "/repair_schedule/cluster/" + clusterName, - EMPTY_PARAMS, - Optional.of("\"" + clusterName + "\""), Response.Status.OK); + callAndExpect( + "GET", + "/repair_schedule/cluster/" + clusterName, + EMPTY_PARAMS, + Optional.of("\"" + clusterName + "\""), + Response.Status.OK); } } @And("^a second daily repair schedule is added for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") public void a_second_daily_repair_schedule_is_added_for_and_keyspace(String clusterName, String keyspace) throws Throwable { + synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - LOG.info("add second daily repair schedule: {}/{}", clusterName, keyspace); - Map params = Maps.newHashMap(); - params.put("clusterName", clusterName); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("intensity", "0.8"); - params.put("scheduleDaysBetween", "1"); - Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = schedule.getId(); + LOG.info("add second daily repair schedule: {}/{}", clusterName, keyspace); + Map params = Maps.newHashMap(); + params.put("clusterName", clusterName); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("intensity", "0.8"); + params.put("scheduleDaysBetween", "1"); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Response response = runner.callReaper("POST", "/repair_schedule", Optional.of(params)); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + RepairScheduleStatus schedule = SimpleReaperClient.parseRepairScheduleStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = schedule.getId(); } } @@ -456,35 +495,34 @@ public void reaper_has_scheduled_repairs_for_cluster_called(int repairAmount, St CLIENTS.parallelStream().forEach(client -> { await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - List schedules = client.getRepairSchedulesForCluster(clusterName); - LOG.info("Got " + schedules.size() + " schedules"); - assertEquals(repairAmount, schedules.size()); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; + try { + List schedules = client.getRepairSchedulesForCluster(clusterName); + LOG.info("Got " + schedules.size() + " schedules"); + assertEquals(repairAmount, schedules.size()); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; }); }); } } - @And("^reaper has (\\d+) scheduled repairs for the last added cluster$") public void reaper_has_scheduled_repairs_for_the_last_added_cluster(int repairAmount) throws Throwable { synchronized (BasicSteps.class) { CLIENTS.parallelStream().forEach(client -> { await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - List schedules = client.getRepairSchedulesForCluster(TestContext.TEST_CLUSTER); - LOG.info("Got " + schedules.size() + " schedules"); - assertEquals(repairAmount, schedules.size()); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; + try { + List schedules = client.getRepairSchedulesForCluster(TestContext.TEST_CLUSTER); + LOG.info("Got " + schedules.size() + " schedules"); + assertEquals(repairAmount, schedules.size()); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; }); }); } @@ -493,83 +531,87 @@ public void reaper_has_scheduled_repairs_for_the_last_added_cluster(int repairAm @When("^the last added schedule is deleted for cluster called \"([^\"]*)\"$") public void the_last_added_schedule_is_deleted_for_cluster_called(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - LOG.info("pause last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); - Map params = Maps.newHashMap(); - params.put("state", "paused"); - - callAndExpect( - "PUT", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.of("\"" + clusterName + "\""), - Response.Status.OK); - - LOG.info("delete last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); - params.clear(); - params.put("owner", TestContext.TEST_USER); + LOG.info("pause last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); + Map params = Maps.newHashMap(); + params.put("state", "paused"); - callAndExpect("DELETE", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.of("\"" + clusterName + "\""), - Response.Status.OK, Response.Status.NOT_FOUND); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.absent(), - Response.Status.NOT_FOUND); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; - }); + callAndExpect( + "PUT", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.of("\"" + clusterName + "\""), + Response.Status.OK, + Response.Status.NOT_MODIFIED); + + LOG.info("delete last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); + params.clear(); + params.put("owner", TestContext.TEST_USER); + + callAndExpect("DELETE", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.of("\"" + clusterName + "\""), + Response.Status.OK, + Response.Status.NOT_FOUND); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( + "DELETE", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.absent(), + Response.Status.NOT_FOUND); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; + }); } } @When("^the last added schedule is deleted for the last added cluster$") public void the_last_added_schedule_is_deleted_for_the_last_added_cluster() throws Throwable { synchronized (BasicSteps.class) { - LOG.info("pause last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); - Map params = Maps.newHashMap(); - params.put("state", "paused"); + LOG.info("pause last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); + Map params = Maps.newHashMap(); + params.put("state", "paused"); - callAndExpect( - "PUT", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.OK, Response.Status.NOT_MODIFIED); + callAndExpect( + "PUT", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.OK, + Response.Status.NOT_MODIFIED); - LOG.info("delete last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); - params.clear(); - params.put("owner", TestContext.TEST_USER); + LOG.info("delete last added repair schedule with id: {}", TestContext.LAST_MODIFIED_ID); + params.clear(); + params.put("owner", TestContext.TEST_USER); - callAndExpect( - "DELETE", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.OK, Response.Status.NOT_FOUND); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.absent(), - Response.Status.NOT_FOUND); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; - }); + callAndExpect( + "DELETE", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.OK, + Response.Status.NOT_FOUND); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( + "DELETE", + "/repair_schedule/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.absent(), + Response.Status.NOT_FOUND); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; + }); } } @@ -579,8 +621,8 @@ public void all_added_schedules_are_deleted_for_the_last_added_cluster() throws final Set schedules = Sets.newConcurrentHashSet(); RUNNERS.parallelStream().forEach(runner -> { - Response response = - runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); + Response response + = runner.callReaper("GET", "/repair_schedule/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); String responseData = response.readEntity(String.class); @@ -593,36 +635,39 @@ public void all_added_schedules_are_deleted_for_the_last_added_cluster() throws params.put("state", "paused"); callAndExpect( - "PUT", - "/repair_schedule/" + schedule.getId(), - Optional.of(params), - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.OK, Response.Status.NOT_MODIFIED, Response.Status.NOT_FOUND); + "PUT", + "/repair_schedule/" + schedule.getId(), + Optional.of(params), + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.OK, + Response.Status.NOT_MODIFIED, + Response.Status.NOT_FOUND); LOG.info("delete last added repair schedule with id: {}", schedule.getId()); params.clear(); params.put("owner", TestContext.TEST_USER); callAndExpect( + "DELETE", + "/repair_schedule/" + schedule.getId(), + Optional.of(params), + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.OK, + Response.Status.NOT_FOUND); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( "DELETE", "/repair_schedule/" + schedule.getId(), Optional.of(params), - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.OK, Response.Status.NOT_FOUND); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/repair_schedule/" + schedule.getId(), - Optional.of(params), - Optional.absent(), - Response.Status.NOT_FOUND); - return true; - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } + Optional.absent(), + Response.Status.NOT_FOUND); + return true; + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } }); }); } @@ -631,153 +676,152 @@ public void all_added_schedules_are_deleted_for_the_last_added_cluster() throws @And("^deleting cluster called \"([^\"]*)\" fails$") public void deleting_cluster_called_fails(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "DELETE", - "/cluster/" + clusterName, - EMPTY_PARAMS, - Optional.of("\"" + clusterName + "\""), - Response.Status.FORBIDDEN); + callAndExpect( + "DELETE", + "/cluster/" + clusterName, + EMPTY_PARAMS, + Optional.of("\"" + clusterName + "\""), + Response.Status.FORBIDDEN); } } @And("^deleting the last added cluster fails$") public void deleting_the_last_added_cluster_fails() throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "DELETE", - "/cluster/" + TestContext.TEST_CLUSTER, - EMPTY_PARAMS, - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.FORBIDDEN); + callAndExpect( + "DELETE", + "/cluster/" + TestContext.TEST_CLUSTER, + EMPTY_PARAMS, + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.FORBIDDEN); } } @And("^cluster called \"([^\"]*)\" is deleted$") public void cluster_called_is_deleted(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "DELETE", - "/cluster/" + clusterName, - EMPTY_PARAMS, - Optional.absent(), - Response.Status.OK, Response.Status.NOT_FOUND); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/cluster/" + clusterName, - EMPTY_PARAMS, - Optional.absent(), - Response.Status.NOT_FOUND); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; - }); + callAndExpect( + "DELETE", + "/cluster/" + clusterName, + EMPTY_PARAMS, + Optional.absent(), + Response.Status.OK, + Response.Status.NOT_FOUND); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( + "DELETE", + "/cluster/" + clusterName, + EMPTY_PARAMS, + Optional.absent(), + Response.Status.NOT_FOUND); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; + }); } } @And("^the last added cluster is deleted$") public void cluster_called_is_deleted() throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "DELETE", - "/cluster/" + TestContext.TEST_CLUSTER, - EMPTY_PARAMS, - Optional.absent(), - Response.Status.OK, Response.Status.NOT_FOUND); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/cluster/" + TestContext.TEST_CLUSTER, - EMPTY_PARAMS, - Optional.absent(), - Response.Status.NOT_FOUND); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; - }); + callAndExpect( + "DELETE", + "/cluster/" + TestContext.TEST_CLUSTER, + EMPTY_PARAMS, + Optional.absent(), + Response.Status.OK, Response.Status.NOT_FOUND); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( + "DELETE", + "/cluster/" + TestContext.TEST_CLUSTER, + EMPTY_PARAMS, + Optional.absent(), + Response.Status.NOT_FOUND); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; + }); } } @Then("^reaper has no cluster called \"([^\"]*)\" in storage$") public void reaper_has_no_cluster_called_in_storage(String clusterName) throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "GET", - "/cluster/" + clusterName, - Optional.>absent(), - Optional.absent(), - Response.Status.NOT_FOUND); + callAndExpect( + "GET", + "/cluster/" + clusterName, + Optional.>absent(), + Optional.absent(), + Response.Status.NOT_FOUND); } } - @Then("^reaper has no longer the last added cluster in storage$") public void reaper_has_no_longer_the_last_added_cluster_in_storage() throws Throwable { synchronized (BasicSteps.class) { - callAndExpect( - "GET", - "/cluster/" + TestContext.TEST_CLUSTER, - Optional.>absent(), - Optional.absent(), - Response.Status.NOT_FOUND); + callAndExpect( + "GET", + "/cluster/" + TestContext.TEST_CLUSTER, + Optional.>absent(), + Optional.absent(), + Response.Status.NOT_FOUND); } } @And("^a new repair is added for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") public void a_new_repair_is_added_for_and_keyspace(String clusterName, String keyspace) throws Throwable { synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", clusterName); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - Response response = - runner.callReaper("POST", "/repair_run", Optional.of(params)); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", clusterName); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); } } @When("^a new repair is added for the last added cluster and keyspace \"([^\"]*)\"$") public void a_new_repair_is_added_for_the_last_added_cluster_and_keyspace(String keyspace) throws Throwable { synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", TestContext.TEST_CLUSTER); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", TestContext.TEST_CLUSTER); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); } } @When("^a new incremental repair is added for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") public void a_new_incremental_repair_is_added_for_and_keyspace(String clusterName, String keyspace) throws Throwable { synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", clusterName); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("incrementalRepair", Boolean.TRUE.toString()); - Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", clusterName); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("incrementalRepair", Boolean.TRUE.toString()); + Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); } } @@ -785,17 +829,17 @@ public void a_new_incremental_repair_is_added_for_and_keyspace(String clusterNam public void a_new_incremental_repair_is_added_for_the_last_added_cluster_and_keyspace(String keyspace) throws Throwable { synchronized (BasicSteps.class) { - ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); - Map params = Maps.newHashMap(); - params.put("clusterName", TestContext.TEST_CLUSTER); - params.put("keyspace", keyspace); - params.put("owner", TestContext.TEST_USER); - params.put("incrementalRepair", Boolean.TRUE.toString()); - Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); + ReaperTestJettyRunner runner = RUNNERS.get(RAND.nextInt(RUNNERS.size())); + Map params = Maps.newHashMap(); + params.put("clusterName", TestContext.TEST_CLUSTER); + params.put("keyspace", keyspace); + params.put("owner", TestContext.TEST_USER); + params.put("incrementalRepair", Boolean.TRUE.toString()); + Response response = runner.callReaper("POST", "/repair_run", Optional.of(params)); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); } } @@ -816,7 +860,7 @@ public void reaper_has_repairs_for_cluster_called(int runAmount, String clusterN public void reaper_has_repairs_for_the_last_added_cluster(int runAmount) throws Throwable { synchronized (BasicSteps.class) { RUNNERS.parallelStream().forEach(runner -> { - Response response = runner.callReaper("GET", "/repair_run/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); + Response response = runner.callReaper("GET", "/repair_run/cluster/" + TestContext.TEST_CLUSTER, EMPTY_PARAMS); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); String responseData = response.readEntity(String.class); List runs = SimpleReaperClient.parseRepairRunStatusListJSON(responseData); @@ -828,37 +872,43 @@ public void reaper_has_repairs_for_the_last_added_cluster(int runAmount) throws @When("^the last added repair run is deleted$") public void the_last_added_repair_run_is_deleted_for_cluster_called() throws Throwable { synchronized (BasicSteps.class) { - LOG.info("delete last added repair run with id: {}", TestContext.LAST_MODIFIED_ID); - Map params = Maps.newHashMap(); - params.put("owner", TestContext.TEST_USER); + LOG.info("delete last added repair run with id: {}", TestContext.LAST_MODIFIED_ID); + Map params = Maps.newHashMap(); + params.put("owner", TestContext.TEST_USER); - callAndExpect( - "DELETE", - "/repair_run/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), - Response.Status.OK, Response.Status.NOT_FOUND, Response.Status.FORBIDDEN); - - await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { - try { - callAndExpect( - "DELETE", - "/repair_run/" + TestContext.LAST_MODIFIED_ID, - Optional.of(params), - Optional.absent(), - Response.Status.NOT_FOUND); - } catch (AssertionError ex) { - LOG.warn(ex.getMessage()); - return false; - } - return true; - }); + callAndExpect( + "DELETE", + "/repair_run/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.of("\"" + TestContext.TEST_CLUSTER + "\""), + Response.Status.OK, + Response.Status.NOT_FOUND, + Response.Status.FORBIDDEN); + + await().with().pollInterval(1, SECONDS).atMost(1, MINUTES).until(() -> { + try { + callAndExpect( + "DELETE", + "/repair_run/" + TestContext.LAST_MODIFIED_ID, + Optional.of(params), + Optional.absent(), + Response.Status.NOT_FOUND); + } catch (AssertionError ex) { + LOG.warn(ex.getMessage()); + return false; + } + return true; + }); } } - @When("^a new daily \"([^\"]*)\" repair schedule is added that already exists for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") - public void a_new_daily_repair_schedule_is_added_that_already_exists_for(String repairType, String clusterName, String keyspace) - throws Throwable { + @When("^a new daily \"([^\"]*)\" repair schedule is added " + + "that already exists for \"([^\"]*)\" and keyspace \"([^\"]*)\"$") + public void a_new_daily_repair_schedule_is_added_that_already_exists_for( + String repairType, + String clusterName, + String keyspace) throws Throwable { + synchronized (BasicSteps.class) { RUNNERS.parallelStream().forEach(runner -> { Map params = Maps.newHashMap(); @@ -875,42 +925,42 @@ public void a_new_daily_repair_schedule_is_added_that_already_exists_for(String } } - @And("^the last added repair is activated$") - public void the_last_added_repair_is_activated_for() throws Throwable { + @And("^the last added repair is activated$") + public void the_last_added_repair_is_activated_for() throws Throwable { synchronized (BasicSteps.class) { final AtomicBoolean set = new AtomicBoolean(false); RUNNERS.parallelStream().forEach(runner -> { Response response = runner.callReaper( - "PUT", - "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=RUNNING", - Optional.of(Maps.newHashMap())); + "PUT", + "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=RUNNING", + Optional.of(Maps.newHashMap())); Assertions.assertThat(ImmutableList.of( - Response.Status.OK.getStatusCode(), - Response.Status.NOT_MODIFIED.getStatusCode())) + Response.Status.OK.getStatusCode(), + Response.Status.NOT_MODIFIED.getStatusCode())) .contains(response.getStatus()); if (Response.Status.OK.getStatusCode() == response.getStatus()) { - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); - set.compareAndSet(false, true); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); + set.compareAndSet(false, true); } }); Assertions.assertThat(set.get()); callAndExpect( - "PUT", - "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=RUNNING", - Optional.absent(), - Optional.absent(), - Response.Status.NOT_MODIFIED); + "PUT", + "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=RUNNING", + Optional.absent(), + Optional.absent(), + Response.Status.NOT_MODIFIED); } - } + } - @When("^the last added repair is stopped$") - public void the_last_added_repair_is_stopped_for() throws Throwable { + @When("^the last added repair is stopped$") + public void the_last_added_repair_is_stopped_for() throws Throwable { synchronized (BasicSteps.class) { final AtomicBoolean set = new AtomicBoolean(false); // given "state" is same as the current run state @@ -918,41 +968,44 @@ public void the_last_added_repair_is_stopped_for() throws Throwable { Map params = Maps.newHashMap(); Response response = runner.callReaper( - "PUT", - "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=PAUSED", - Optional.of(params)); + "PUT", + "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=PAUSED", + Optional.of(params)); Assertions.assertThat(ImmutableList.of( - Response.Status.OK.getStatusCode(), - Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), - Response.Status.NOT_MODIFIED.getStatusCode())) + Response.Status.OK.getStatusCode(), + Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), + Response.Status.NOT_MODIFIED.getStatusCode())) .contains(response.getStatus()); if (Response.Status.OK.getStatusCode() == response.getStatus()) { - String responseData = response.readEntity(String.class); - RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); - TestContext.LAST_MODIFIED_ID = run.getId(); - set.compareAndSet(false, true); + String responseData = response.readEntity(String.class); + RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); + TestContext.LAST_MODIFIED_ID = run.getId(); + set.compareAndSet(false, true); } }); Assertions.assertThat(set.get()); callAndExpect( - "PUT", - "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=PAUSED", - Optional.absent(), - Optional.absent(), - Response.Status.NOT_MODIFIED, - Response.Status.METHOD_NOT_ALLOWED); + "PUT", + "/repair_run/" + TestContext.LAST_MODIFIED_ID + "?state=PAUSED", + Optional.absent(), + Optional.absent(), + Response.Status.NOT_MODIFIED, + Response.Status.METHOD_NOT_ALLOWED); } - } + } - @And("^we wait for at least (\\d+) segments to be repaired$") - public void we_wait_for_at_least_segments_to_be_repaired(int nbSegmentsToBeRepaired) throws Throwable { + @And("^we wait for at least (\\d+) segments to be repaired$") + public void we_wait_for_at_least_segments_to_be_repaired(int nbSegmentsToBeRepaired) throws Throwable { synchronized (BasicSteps.class) { RUNNERS.parallelStream().forEach(runner -> { await().with().pollInterval(10, SECONDS).atMost(2, MINUTES).until(() -> { - Response response = runner.callReaper("GET", "/repair_run/" + TestContext.LAST_MODIFIED_ID, EMPTY_PARAMS); + + Response response = runner + .callReaper("GET", "/repair_run/" + TestContext.LAST_MODIFIED_ID, EMPTY_PARAMS); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); String responseData = response.readEntity(String.class); RepairRunStatus run = SimpleReaperClient.parseRepairRunStatusJSON(responseData); @@ -960,43 +1013,44 @@ public void we_wait_for_at_least_segments_to_be_repaired(int nbSegmentsToBeRepai }); }); } - } + } private static void createKeyspace(String keyspaceName) { - try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { - tmpSession.execute("CREATE KEYSPACE IF NOT EXISTS " + keyspaceName - + " WITH replication = {" + buildNetworkTopologyStrategyString(cluster) + "}"); - } + try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { + tmpSession.execute("CREATE KEYSPACE IF NOT EXISTS " + keyspaceName + + " WITH replication = {" + buildNetworkTopologyStrategyString(cluster) + "}"); + } } static String buildNetworkTopologyStrategyString(Cluster cluster) { - Map ntsMap = Maps.newHashMap(); + Map ntsMap = Maps.newHashMap(); for (Host host : cluster.getMetadata().getAllHosts()) { - String dc = host.getDatacenter(); - ntsMap.put(dc, 1+ ntsMap.getOrDefault(dc, 0)); + String dc = host.getDatacenter(); + ntsMap.put(dc, 1 + ntsMap.getOrDefault(dc, 0)); } StringBuilder builder = new StringBuilder("'class':'NetworkTopologyStrategy',"); - for (Map.Entry e : ntsMap.entrySet()) { - builder.append("'").append(e.getKey()).append("':").append(e.getValue()).append(","); + for (Map.Entry e : ntsMap.entrySet()) { + builder.append("'").append(e.getKey()).append("':").append(e.getValue()).append(","); } - return builder.substring(0, builder.length()-1); + return builder.substring(0, builder.length() - 1); } private static Cluster buildCluster() { return Cluster.builder() - .addContactPoint("127.0.0.1") - .withSocketOptions(new SocketOptions().setConnectTimeoutMillis(20000).setReadTimeoutMillis(40000)) - .build(); + .addContactPoint("127.0.0.1") + .withSocketOptions(new SocketOptions().setConnectTimeoutMillis(20000).setReadTimeoutMillis(40000)) + .build(); } private static void createTable(String keyspaceName, String tableName) { - try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { + try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { + tmpSession.execute( + "CREATE TABLE IF NOT EXISTS " + keyspaceName + "." + tableName + "(id int PRIMARY KEY, value text)"); + + for (int i = 0; i < 100; i++) { tmpSession.execute( - "CREATE TABLE IF NOT EXISTS " + keyspaceName + "." + tableName + "(id int PRIMARY KEY, value text)"); - for(int i=0;i<100;i++){ - tmpSession.execute( - "INSERT INTO " + keyspaceName + "." + tableName + "(id, value) VALUES(" + i + ",'" + i + "')"); - } + "INSERT INTO " + keyspaceName + "." + tableName + "(id, value) VALUES(" + i + ",'" + i + "')"); } + } } } diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperCassandraIT.java b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperCassandraIT.java index 3e33401a8..fa09f74a4 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperCassandraIT.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperCassandraIT.java @@ -11,27 +11,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.acceptance; -import java.io.IOException; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Session; import com.datastax.driver.core.SocketOptions; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; - import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; -import java.util.List; -import java.util.Random; -import java.util.concurrent.CopyOnWriteArrayList; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; @@ -40,74 +40,76 @@ @RunWith(Cucumber.class) @CucumberOptions( features = "classpath:com.spotify.reaper.acceptance/integration_reaper_functionality.feature" -) -public class ReaperCassandraIT { + ) +public final class ReaperCassandraIT { + private static final Logger LOG = LoggerFactory.getLogger(ReaperCassandraIT.class); private static final List RUNNER_INSTANCES = new CopyOnWriteArrayList<>(); - private static final String CASS_CONFIG_FILE="cassandra-reaper-cassandra-at.yaml"; + private static final String CASS_CONFIG_FILE = "cassandra-reaper-cassandra-at.yaml"; private static final Random RAND = new Random(System.nanoTime()); private static Thread GRIM_REAPER; + private ReaperCassandraIT() {} @BeforeClass public static void setUp() throws Exception { LOG.info( - "setting up testing Reaper runner with {} seed hosts defined and cassandra storage", - TestContext.TEST_CLUSTER_SEED_HOSTS.size()); + "setting up testing Reaper runner with {} seed hosts defined and cassandra storage", + TestContext.TEST_CLUSTER_SEED_HOSTS.size()); int minReaperInstances = Integer.getInteger("grim.reaper.min", 1); int maxReaperInstances = Integer.getInteger("grim.reaper.max", minReaperInstances); initSchema(); - for ( int i =0 ; i < minReaperInstances ; ++i) { - ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); - runner.setup(new AppContext(), CASS_CONFIG_FILE); - RUNNER_INSTANCES.add(runner); - BasicSteps.addReaperRunner(runner); + for (int i = 0; i < minReaperInstances; ++i) { + ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); + runner.setup(new AppContext(), CASS_CONFIG_FILE); + RUNNER_INSTANCES.add(runner); + BasicSteps.addReaperRunner(runner); } GRIM_REAPER = new Thread(() -> { - Thread.currentThread().setName("GRIM REAPER"); - while (!Thread.currentThread().isInterrupted()) { //keep adding/removing reaper instances while test is running - try { - if (maxReaperInstances > RUNNER_INSTANCES.size()) { - ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); - ReaperJettyTestSupport instance = runner.setup(new AppContext(), CASS_CONFIG_FILE); - RUNNER_INSTANCES.add(runner); - Thread.sleep(100); - BasicSteps.addReaperRunner(runner); - } else { - int remove = minReaperInstances + RAND.nextInt(maxReaperInstances-minReaperInstances); - ReaperTestJettyRunner runner = RUNNER_INSTANCES.get(remove); - BasicSteps.removeReaperRunner(runner); - Thread.sleep(200); - runner.runnerInstance.after(); - RUNNER_INSTANCES.remove(runner); - } - } catch (Exception ex) { - LOG.error("failed adding/removing reaper instance", ex); - } + Thread.currentThread().setName("GRIM REAPER"); + while (!Thread.currentThread().isInterrupted()) { //keep adding/removing reaper instances while test is running + try { + if (maxReaperInstances > RUNNER_INSTANCES.size()) { + ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); + ReaperJettyTestSupport instance = runner.setup(new AppContext(), CASS_CONFIG_FILE); + RUNNER_INSTANCES.add(runner); + Thread.sleep(100); + BasicSteps.addReaperRunner(runner); + } else { + int remove = minReaperInstances + RAND.nextInt(maxReaperInstances - minReaperInstances); + ReaperTestJettyRunner runner = RUNNER_INSTANCES.get(remove); + BasicSteps.removeReaperRunner(runner); + Thread.sleep(200); + runner.runnerInstance.after(); + RUNNER_INSTANCES.remove(runner); + } + } catch (RuntimeException | InterruptedException ex) { + LOG.error("failed adding/removing reaper instance", ex); } + } }); if (minReaperInstances < maxReaperInstances) { - GRIM_REAPER.start(); + GRIM_REAPER.start(); } } - public static void initSchema() throws IOException { - try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { - await().with().pollInterval(3, SECONDS).atMost(2, MINUTES).until(() -> { - try { - tmpSession.execute("DROP KEYSPACE IF EXISTS reaper_db"); - return true; - } catch (Exception ex) { - return false; - } - }); - tmpSession.execute( - "CREATE KEYSPACE reaper_db WITH replication = {" + BasicSteps.buildNetworkTopologyStrategyString(cluster) + "}"); - } + try (Cluster cluster = buildCluster(); Session tmpSession = cluster.connect()) { + await().with().pollInterval(3, SECONDS).atMost(2, MINUTES).until(() -> { + try { + tmpSession.execute("DROP KEYSPACE IF EXISTS reaper_db"); + return true; + } catch (RuntimeException ex) { + return false; + } + }); + tmpSession.execute( + "CREATE KEYSPACE reaper_db WITH replication = {" + BasicSteps.buildNetworkTopologyStrategyString(cluster) + + "}"); + } } @AfterClass @@ -117,11 +119,10 @@ public static void tearDown() { RUNNER_INSTANCES.forEach(r -> r.runnerInstance.after()); } - private static Cluster buildCluster() { return Cluster.builder() - .addContactPoint("127.0.0.1") - .withSocketOptions(new SocketOptions().setConnectTimeoutMillis(20000).setReadTimeoutMillis(40000)) - .build(); + .addContactPoint("127.0.0.1") + .withSocketOptions(new SocketOptions().setConnectTimeoutMillis(20000).setReadTimeoutMillis(40000)) + .build(); } } diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperH2IT.java b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperH2IT.java index 076228974..bc365579a 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperH2IT.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperH2IT.java @@ -11,34 +11,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.acceptance; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; + +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; -import cucumber.api.CucumberOptions; -import cucumber.api.junit.Cucumber; @RunWith(Cucumber.class) @CucumberOptions( features = "classpath:com.spotify.reaper.acceptance/integration_reaper_functionality.feature" -) -public class ReaperH2IT { + ) +public final class ReaperH2IT { + private static final Logger LOG = LoggerFactory.getLogger(ReaperH2IT.class); private static ReaperJettyTestSupport runnerInstance; - private static final String H2_CONFIG_FILE="cassandra-reaper-h2-at.yaml"; + private static final String H2_CONFIG_FILE = "cassandra-reaper-h2-at.yaml"; + private ReaperH2IT() {} @BeforeClass public static void setUp() throws Exception { - LOG.info("setting up testing Reaper runner with {} seed hosts defined and H2 storage", + LOG.info( + "setting up testing Reaper runner with {} seed hosts defined and H2 storage", TestContext.TEST_CLUSTER_SEED_HOSTS.size()); + AppContext context = new AppContext(); ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); runnerInstance = runner.setup(context, H2_CONFIG_FILE); diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperIT.java b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperIT.java index 60dc3af5b..b37722e2e 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperIT.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperIT.java @@ -11,34 +11,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.acceptance; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.spotify.reaper.AppContext; import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @RunWith(Cucumber.class) @CucumberOptions( features = "classpath:com.spotify.reaper.acceptance/integration_reaper_functionality.feature" -) -public class ReaperIT { + ) +public final class ReaperIT { + private static final Logger LOG = LoggerFactory.getLogger(ReaperIT.class); private static ReaperJettyTestSupport runnerInstance; - private static final String MEMORY_CONFIG_FILE="cassandra-reaper-at.yaml"; + private static final String MEMORY_CONFIG_FILE = "cassandra-reaper-at.yaml"; + private ReaperIT() {} @BeforeClass public static void setUp() throws Exception { - LOG.info("setting up testing Reaper runner with {} seed hosts defined and memory storage", + LOG.info( + "setting up testing Reaper runner with {} seed hosts defined and memory storage", TestContext.TEST_CLUSTER_SEED_HOSTS.size()); + AppContext context = new AppContext(); ReaperTestJettyRunner runner = new ReaperTestJettyRunner(); runnerInstance = runner.setup(context, MEMORY_CONFIG_FILE); diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperPostgresIT.java b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperPostgresIT.java index 127b1901d..202d5f4f8 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperPostgresIT.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperPostgresIT.java @@ -11,29 +11,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper.acceptance; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package com.spotify.reaper.acceptance; import com.spotify.reaper.AppContext; import com.spotify.reaper.acceptance.ReaperTestJettyRunner.ReaperJettyTestSupport; import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @RunWith(Cucumber.class) @CucumberOptions( features = "classpath:com.spotify.reaper.acceptance/integration_reaper_functionality.feature" -) -public class ReaperPostgresIT { + ) +public final class ReaperPostgresIT { + private static final Logger LOG = LoggerFactory.getLogger(ReaperPostgresIT.class); private static ReaperJettyTestSupport runnerInstance; - private static final String POSTGRES_CONFIG_FILE="cassandra-reaper-postgres-at.yaml"; + private static final String POSTGRES_CONFIG_FILE = "cassandra-reaper-postgres-at.yaml"; + private ReaperPostgresIT() {} @BeforeClass public static void setUp() throws Exception { diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperTestJettyRunner.java b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperTestJettyRunner.java index fb3bb0e49..3959cf62f 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperTestJettyRunner.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/ReaperTestJettyRunner.java @@ -1,27 +1,38 @@ +/* + * 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.spotify.reaper.acceptance; -import com.google.common.base.Optional; -import com.google.common.io.Resources; import com.spotify.reaper.AppContext; import com.spotify.reaper.ReaperApplication; import com.spotify.reaper.ReaperApplicationConfiguration; import com.spotify.reaper.SimpleReaperClient; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; - import java.io.File; import java.io.IOException; import java.net.ServerSocket; import java.util.Map; - import javax.ws.rs.core.Response; +import com.google.common.base.Optional; +import com.google.common.io.Resources; import io.dropwizard.Application; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.DropwizardTestSupport; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; /** * Simple Reaper application runner for testing purposes. @@ -34,7 +45,10 @@ public final class ReaperTestJettyRunner { private Server jettyServer; private SimpleReaperClient reaperClientInstance; - public ReaperJettyTestSupport setup(AppContext testContext, String yamlConfigFile) throws Exception { + public ReaperTestJettyRunner() { + } + + public ReaperJettyTestSupport setup(AppContext testContext, String yamlConfigFile) { if (runnerInstance == null) { runnerInstance = new ReaperJettyTestSupport(Resources.getResource(yamlConfigFile).getPath(), testContext); @@ -65,48 +79,46 @@ public SimpleReaperClient getClient() { return reaperClientInstance; } - public ReaperTestJettyRunner() { - } - public int getLocalPort() { assert jettyServer != null : "service not initialized, call setup() first"; return ((ServerConnector) jettyServer.getConnectors()[0]).getLocalPort(); } - public final static class ReaperJettyTestSupport extends DropwizardTestSupport { - AppContext context; + public static final class ReaperJettyTestSupport extends DropwizardTestSupport { - private ReaperJettyTestSupport(String configFile, AppContext context) { - super(ReaperApplication.class, - new File(configFile).getAbsolutePath(), - ConfigOverride.config("server.adminConnectors[0].port", "" + getAnyAvailablePort()), - ConfigOverride.config("server.applicationConnectors[0].port", "" + getAnyAvailablePort()) - ); - this.context = context; - } + AppContext context; - @Override - public Application newApplication() { - return new ReaperApplication(this.context); - } + private ReaperJettyTestSupport(String configFile, AppContext context) { + super(ReaperApplication.class, + new File(configFile).getAbsolutePath(), + ConfigOverride.config("server.adminConnectors[0].port", "" + getAnyAvailablePort()), + ConfigOverride.config("server.applicationConnectors[0].port", "" + getAnyAvailablePort()) + ); + this.context = context; + } - @Override - public void after() { - context.isRunning.set(false); - try { - Thread.sleep(100); - } catch (InterruptedException ex) {} - super.after(); - } + @Override + public Application newApplication() { + return new ReaperApplication(this.context); + } - private static int getAnyAvailablePort() { - // this method doesn't actually reserve the ports - // so subsequent calls may well return the same number - try (ServerSocket s = new ServerSocket(0)) { - return s.getLocalPort(); - } catch (IOException ex) { - throw new IllegalStateException("no available ports", ex); - } + @Override + public void after() { + context.isRunning.set(false); + try { + Thread.sleep(100); + } catch (InterruptedException expected) { } + super.after(); + } + + private static int getAnyAvailablePort() { + // this method doesn't actually reserve the ports + // so subsequent calls may well return the same number + try (ServerSocket s = new ServerSocket(0)) { + return s.getLocalPort(); + } catch (IOException ex) { + throw new IllegalStateException("no available ports", ex); + } } } diff --git a/src/server/src/test/java/com/spotify/reaper/acceptance/TestContext.java b/src/server/src/test/java/com/spotify/reaper/acceptance/TestContext.java index 11f9943e9..1252510c1 100644 --- a/src/server/src/test/java/com/spotify/reaper/acceptance/TestContext.java +++ b/src/server/src/test/java/com/spotify/reaper/acceptance/TestContext.java @@ -1,3 +1,17 @@ +/* + * 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.spotify.reaper.acceptance; import java.util.HashMap; @@ -9,9 +23,9 @@ * Helper class for holding acceptance test scenario state. * Contains also methods for getting related resources for testing, like mocks etc. */ -public class TestContext { +public final class TestContext { - public static String TEST_USER = "test_user"; + public static String TEST_USER = "test_user"; public static String SEED_HOST; public static String TEST_CLUSTER; @@ -24,13 +38,15 @@ public class TestContext { /* Testing cluster name mapped to keyspace name mapped to tables list. */ public static Map>> TEST_CLUSTER_INFO = new HashMap<>(); + private TestContext() {} + /** * Adds testing cluster information for testing purposes. * Used to create mocks and prepare testing access for added testing clusters. */ public static void addClusterInfo(String clusterName, String keyspace, Set tables) { if (!TEST_CLUSTER_INFO.containsKey(clusterName)) { - TEST_CLUSTER_INFO.put(clusterName, new HashMap>()); + TEST_CLUSTER_INFO.put(clusterName, new HashMap<>()); } TEST_CLUSTER_INFO.get(clusterName).put(keyspace, tables); } @@ -39,5 +55,4 @@ public static void addSeedHostToClusterMapping(String seedHost, String clusterNa TEST_CLUSTER_SEED_HOSTS.put(seedHost, clusterName); } - } diff --git a/src/server/src/test/java/com/spotify/reaper/resources/CommonToolsTest.java b/src/server/src/test/java/com/spotify/reaper/resources/CommonToolsTest.java index 9fcf0cd58..884d9e690 100644 --- a/src/server/src/test/java/com/spotify/reaper/resources/CommonToolsTest.java +++ b/src/server/src/test/java/com/spotify/reaper/resources/CommonToolsTest.java @@ -1,24 +1,23 @@ +/* + * 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.spotify.reaper.resources; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.junit.Test; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxProxy; import com.spotify.reaper.core.RepairUnit; import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.unit.service.RepairRunnerTest; - -import jersey.repackaged.com.google.common.collect.Maps; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.HashSet; @@ -26,19 +25,29 @@ import java.util.Map; import java.util.Set; -public class CommonToolsTest { +import com.google.common.collect.Sets; +import jersey.repackaged.com.google.common.collect.Maps; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class CommonToolsTest { @Test public void testDateTimeToISO8601() { DateTime dateTime = new DateTime(2015, 2, 20, 15, 24, 45, DateTimeZone.UTC); - assertEquals("2015-02-20T15:24:45Z", CommonTools.dateTimeToISO8601(dateTime)); + assertEquals("2015-02-20T15:24:45Z", CommonTools.dateTimeToIso8601(dateTime)); } @Test public void testParseSeedHost() { String seedHostStringList = "127.0.0.1 , 127.0.0.2, 127.0.0.3"; Set seedHostSet = CommonTools.parseSeedHosts(seedHostStringList); - Set seedHostExpectedSet = Sets.newHashSet("127.0.0.2","127.0.0.1","127.0.0.3"); + Set seedHostExpectedSet = Sets.newHashSet("127.0.0.2", "127.0.0.1", "127.0.0.3"); assertEquals(seedHostSet, seedHostExpectedSet); } @@ -46,7 +55,7 @@ public void testParseSeedHost() { @Test public void buildEndpointToRangeMapTest() { Map, List> rangeToEndpoint = Maps.newHashMap(); - rangeToEndpoint.put(Arrays.asList("1","2"), Arrays.asList("node1","node2","node3")); + rangeToEndpoint.put(Arrays.asList("1", "2"), Arrays.asList("node1", "node2", "node3")); rangeToEndpoint.put(Arrays.asList("2", "4"), Arrays.asList("node1", "node2", "node3")); rangeToEndpoint.put(Arrays.asList("4", "6"), Arrays.asList("node1")); rangeToEndpoint.put(Arrays.asList("6", "8"), Arrays.asList("node1", "node2")); @@ -99,4 +108,4 @@ public void filterSegmentsByNodesTest() throws ReaperException { assertEquals(filtered.size(), 6); } -} \ No newline at end of file +} diff --git a/src/server/src/test/java/com/spotify/reaper/resources/view/NodesStatusTest.java b/src/server/src/test/java/com/spotify/reaper/resources/view/NodesStatusTest.java index 4c7d4d552..f0ef7be7f 100644 --- a/src/server/src/test/java/com/spotify/reaper/resources/view/NodesStatusTest.java +++ b/src/server/src/test/java/com/spotify/reaper/resources/view/NodesStatusTest.java @@ -1,169 +1,185 @@ -package com.spotify.reaper.resources.view; +/* + * 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. + */ -import org.junit.Test; +package com.spotify.reaper.resources.view; -import com.spotify.reaper.resources.view.NodesStatus.EndpointState; -import jersey.repackaged.com.google.common.collect.Maps; -import static org.junit.Assert.*; +import com.spotify.reaper.resources.view.NodesStatus.EndpointState; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -public class NodesStatusTest { +import jersey.repackaged.com.google.common.collect.Maps; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public final class NodesStatusTest { @Test public void testParseEndpoint22StatusString() { Map simpleStates = Maps.newHashMap(); - StringBuilder endpointsStatusString = new StringBuilder().append("/127.0.0.1") - .append(" generation:1496849190 ") - .append(" heartbeat:1231900 ") - .append(" STATUS:14:NORMAL,-9223372036854775808 ") - .append(" LOAD:1231851:4043215.0 ") - .append(" SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:6:datacenter1 ") - .append(" RACK:8:rack1 ") - .append(" RELEASE_VERSION:4:3.0.8 ") - .append(" RPC_ADDRESS:3:127.0.0.1 ") - .append(" SEVERITY:1231899:0.0 ") - .append(" NET_VERSION:1:10 ") - .append(" HOST_ID:2:f091f82b-ce2c-40ee-b30c-6e761e94e821 ") - .append(" RPC_READY:16:true ") - .append(" TOKENS:13: \r") - .append("/127.0.0.2") - .append(" generation:1497347537 ") - .append(" heartbeat:27077 ") - .append(" STATUS:14:NORMAL,-3074457345618258603 ") - .append(" LOAD:1231851:3988763.0 ") - .append(" SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:6:datacenter2 ") - .append(" RACK:8:rack2 ") - .append(" RELEASE_VERSION:4:3.0.8 ") - .append(" RPC_ADDRESS:3:127.0.0.2 ") - .append(" SEVERITY:1231899:0.0 ") - .append(" NET_VERSION:1:10 ") - .append(" HOST_ID:2:08f819b5-d96f-444e-9d4d-ec4136e1b716 ") - .append(" RPC_READY:16:true") - .append(" TOKENS:13: \r") - .append("/127.0.0.3") - .append(" generation:1496849191 ") - .append(" heartbeat:1230183 ") - .append(" STATUS:16:NORMAL,3074457345618258602 ") - .append(" LOAD:1230134:3.974144E6") - .append(" SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:6:us-west-1") - .append(" RACK:8:rack3 ") - .append(" RELEASE_VERSION:4:3.0.8 ") - .append(" RPC_ADDRESS:3:127.0.0.3 ") - .append(" SEVERITY:1230182:0.0 ") - .append(" NET_VERSION:1:10 ") - .append(" HOST_ID:2:20769fed-7916-4b7a-a729-8b99bcdc9b95 ") - .append(" RPC_READY:44:true ") - .append(" TOKENS:15: "); + String endpointsStatusString = + "/127.0.0.1" + + " generation:1496849190 " + + " heartbeat:1231900 " + + " STATUS:14:NORMAL,-9223372036854775808 " + + " LOAD:1231851:4043215.0 " + + " SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:6:datacenter1 " + + " RACK:8:rack1 " + + " RELEASE_VERSION:4:3.0.8 " + + " RPC_ADDRESS:3:127.0.0.1 " + + " SEVERITY:1231899:0.0 " + + " NET_VERSION:1:10 " + + " HOST_ID:2:f091f82b-ce2c-40ee-b30c-6e761e94e821 " + + " RPC_READY:16:true " + + " TOKENS:13: \r" + + "/127.0.0.2" + + " generation:1497347537 " + + " heartbeat:27077 " + + " STATUS:14:NORMAL,-3074457345618258603 " + + " LOAD:1231851:3988763.0 " + + " SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:6:datacenter2 " + + " RACK:8:rack2 " + + " RELEASE_VERSION:4:3.0.8 " + + " RPC_ADDRESS:3:127.0.0.2 " + + " SEVERITY:1231899:0.0 " + + " NET_VERSION:1:10 " + + " HOST_ID:2:08f819b5-d96f-444e-9d4d-ec4136e1b716 " + + " RPC_READY:16:true" + + " TOKENS:13: \r" + + "/127.0.0.3" + + " generation:1496849191 " + + " heartbeat:1230183 " + + " STATUS:16:NORMAL,3074457345618258602 " + + " LOAD:1230134:3.974144E6" + + " SCHEMA:10:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:6:us-west-1" + + " RACK:8:rack3 " + + " RELEASE_VERSION:4:3.0.8 " + + " RPC_ADDRESS:3:127.0.0.3 " + + " SEVERITY:1230182:0.0 " + + " NET_VERSION:1:10 " + + " HOST_ID:2:20769fed-7916-4b7a-a729-8b99bcdc9b95 " + + " RPC_READY:44:true " + + " TOKENS:15: "; simpleStates.put("/127.0.0.3", "UP"); simpleStates.put("/127.0.0.1", "DOWN"); - NodesStatus nodesStatus = new NodesStatus("127.0.0.1", endpointsStatusString.toString(), simpleStates); + NodesStatus nodesStatus = new NodesStatus("127.0.0.1", endpointsStatusString, simpleStates); assertEquals(nodesStatus.endpointStates.size(), 1); assertEquals(nodesStatus.endpointStates.get(0).sourceNode, "127.0.0.1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").keySet().size(), 1); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").size(), 1); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).status, "NORMAL - DOWN"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).status, "NORMAL - UNKNOWN"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).status, "NORMAL - UP"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).endpoint, "127.0.0.1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).hostId, "f091f82b-ce2c-40ee-b30c-6e761e94e821"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).tokens, "13"); - assertTrue(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).severity.equals(0.0)); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).releaseVersion, "3.0.8"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).dc, "datacenter2"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).rack, "rack2"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).dc, "us-west-1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).rack, "rack3"); - assertTrue(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).load.equals(3974144.0)); + Map>> endpoints = nodesStatus.endpointStates.get(0).endpoints; + assertEquals(endpoints.get("datacenter1").keySet().size(), 1); + assertEquals(endpoints.get("datacenter1").get("rack1").size(), 1); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).status, "NORMAL - DOWN"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).status, "NORMAL - UNKNOWN"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).status, "NORMAL - UP"); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).endpoint, "127.0.0.1"); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).hostId, "f091f82b-ce2c-40ee-b30c-6e761e94e821"); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).tokens, "13"); + assertTrue(endpoints.get("datacenter1").get("rack1").get(0).severity.equals(0.0)); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).releaseVersion, "3.0.8"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).dc, "datacenter2"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).rack, "rack2"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).dc, "us-west-1"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).rack, "rack3"); + assertTrue(endpoints.get("us-west-1").get("rack3").get(0).load.equals(3974144.0)); } @Test public void testParseEndpoint21StatusString() { Map simpleStates = Maps.newHashMap(); - StringBuilder endpointsStatusString = new StringBuilder().append("/127.0.0.1") - .append(" generation:1496849190 ") - .append(" heartbeat:1231900 ") - .append(" STATUS:NORMAL,-9223372036854775808 ") - .append(" LOAD:4043215.0 ") - .append(" SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:datacenter1 ") - .append(" RACK:rack1 ") - .append(" RELEASE_VERSION:3.0.8 ") - .append(" RPC_ADDRESS:127.0.0.1 ") - .append(" SEVERITY:0.0 ") - .append(" NET_VERSION:10 ") - .append(" HOST_ID:f091f82b-ce2c-40ee-b30c-6e761e94e821 ") - .append(" RPC_READY:true ") - .append(" TOKENS: \r") - .append("/127.0.0.2") - .append(" generation:1497347537 ") - .append(" heartbeat:27077 ") - .append(" STATUS:NORMAL,-3074457345618258603 ") - .append(" LOAD:3988763.0 ") - .append(" SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:datacenter2 ") - .append(" RACK:rack2 ") - .append(" RELEASE_VERSION:3.0.8 ") - .append(" RPC_ADDRESS:127.0.0.2 ") - .append(" SEVERITY:0.0 ") - .append(" NET_VERSION:10 ") - .append(" HOST_ID:08f819b5-d96f-444e-9d4d-ec4136e1b716 ") - .append(" RPC_READY:true \r") - .append("/127.0.0.3") - .append(" generation:1496849191 ") - .append(" heartbeat:1230183 ") - .append(" STATUS:NORMAL,3074457345618258602 ") - .append(" LOAD:3.974144E6") - .append(" SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 ") - .append(" DC:us-west-1") - .append(" RACK:rack3 ") - .append(" RELEASE_VERSION:3.0.8 ") - .append(" RPC_ADDRESS:127.0.0.3 ") - .append(" SEVERITY:0.0 ") - .append(" NET_VERSION:10 ") - .append(" HOST_ID:20769fed-7916-4b7a-a729-8b99bcdc9b95 ") - .append(" RPC_READY:true ") - .append(" TOKENS: "); - - simpleStates.put("/127.0.0.3","UP"); - simpleStates.put("/127.0.0.1","DOWN"); - - - NodesStatus nodesStatus = new NodesStatus("127.0.0.1", endpointsStatusString.toString(), simpleStates); + String endpointsStatusString = + "/127.0.0.1" + + " generation:1496849190 " + + " heartbeat:1231900 " + + " STATUS:NORMAL,-9223372036854775808 " + + " LOAD:4043215.0 " + + " SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:datacenter1 " + + " RACK:rack1 " + + " RELEASE_VERSION:3.0.8 " + + " RPC_ADDRESS:127.0.0.1 " + + " SEVERITY:0.0 " + + " NET_VERSION:10 " + + " HOST_ID:f091f82b-ce2c-40ee-b30c-6e761e94e821 " + + " RPC_READY:true " + + " TOKENS: \r" + + "/127.0.0.2" + + " generation:1497347537 " + + " heartbeat:27077 " + + " STATUS:NORMAL,-3074457345618258603 " + + " LOAD:3988763.0 " + + " SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:datacenter2 " + + " RACK:rack2 " + + " RELEASE_VERSION:3.0.8 " + + " RPC_ADDRESS:127.0.0.2 " + + " SEVERITY:0.0 " + + " NET_VERSION:10 " + + " HOST_ID:08f819b5-d96f-444e-9d4d-ec4136e1b716 " + + " RPC_READY:true \r" + + "/127.0.0.3" + + " generation:1496849191 " + + " heartbeat:1230183 " + + " STATUS:NORMAL,3074457345618258602 " + + " LOAD:3.974144E6" + + " SCHEMA:2de0af6a-bf86-38e0-b62b-474ff6aefb51 " + + " DC:us-west-1" + + " RACK:rack3 " + + " RELEASE_VERSION:3.0.8 " + + " RPC_ADDRESS:127.0.0.3 " + + " SEVERITY:0.0 " + + " NET_VERSION:10 " + + " HOST_ID:20769fed-7916-4b7a-a729-8b99bcdc9b95 " + + " RPC_READY:true " + + " TOKENS: "; - assertEquals(nodesStatus.endpointStates.size(), 1); - assertEquals(nodesStatus.endpointStates.get(0).sourceNode, "127.0.0.1"); + simpleStates.put("/127.0.0.3", "UP"); + simpleStates.put("/127.0.0.1", "DOWN"); + NodesStatus nodesStatus = new NodesStatus("127.0.0.1", endpointsStatusString, simpleStates); + assertEquals(nodesStatus.endpointStates.size(), 1); + assertEquals(nodesStatus.endpointStates.get(0).sourceNode, "127.0.0.1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").keySet().size(), 1); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").size(), 1); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).status, "NORMAL - DOWN"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).status, "NORMAL - UNKNOWN"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).status, "NORMAL - UP"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).endpoint, "127.0.0.1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).hostId, "f091f82b-ce2c-40ee-b30c-6e761e94e821"); - assertTrue(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).severity.equals(0.0)); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter1").get("rack1").get(0).releaseVersion, "3.0.8"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).dc, "datacenter2"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("datacenter2").get("rack2").get(0).rack, "rack2"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).dc, "us-west-1"); - assertEquals(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).rack, "rack3"); - assertTrue(nodesStatus.endpointStates.get(0).endpoints.get("us-west-1").get("rack3").get(0).load.equals(3974144.0)); + Map>> endpoints = nodesStatus.endpointStates.get(0).endpoints; + assertEquals(endpoints.get("datacenter1").keySet().size(), 1); + assertEquals(endpoints.get("datacenter1").get("rack1").size(), 1); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).status, "NORMAL - DOWN"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).status, "NORMAL - UNKNOWN"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).status, "NORMAL - UP"); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).endpoint, "127.0.0.1"); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).hostId, "f091f82b-ce2c-40ee-b30c-6e761e94e821"); + assertTrue(endpoints.get("datacenter1").get("rack1").get(0).severity.equals(0.0)); + assertEquals(endpoints.get("datacenter1").get("rack1").get(0).releaseVersion, "3.0.8"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).dc, "datacenter2"); + assertEquals(endpoints.get("datacenter2").get("rack2").get(0).rack, "rack2"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).dc, "us-west-1"); + assertEquals(endpoints.get("us-west-1").get("rack3").get(0).rack, "rack3"); + assertTrue(endpoints.get("us-west-1").get("rack3").get(0).load.equals(3974144.0)); assertTrue(nodesStatus.endpointStates.get(0).endpointNames.contains("127.0.0.1")); assertTrue(nodesStatus.endpointStates.get(0).endpointNames.contains("127.0.0.2")); diff --git a/src/server/src/test/java/com/spotify/reaper/resources/view/RepairRunStatusTest.java b/src/server/src/test/java/com/spotify/reaper/resources/view/RepairRunStatusTest.java index 435ffef9e..6f47754e5 100644 --- a/src/server/src/test/java/com/spotify/reaper/resources/view/RepairRunStatusTest.java +++ b/src/server/src/test/java/com/spotify/reaper/resources/view/RepairRunStatusTest.java @@ -1,16 +1,27 @@ +/* + * 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.spotify.reaper.resources.view; -import com.google.common.collect.Lists; import com.spotify.reaper.resources.CommonTools; import org.junit.Test; -import java.util.Collection; - -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; -public class RepairRunStatusTest { +public final class RepairRunStatusTest { @Test public void testRoundIntensity() throws Exception { diff --git a/src/server/src/test/java/com/spotify/reaper/resources/view/RepairScheduleStatusTest.java b/src/server/src/test/java/com/spotify/reaper/resources/view/RepairScheduleStatusTest.java index b4dfb80bf..e7fc2e8e0 100644 --- a/src/server/src/test/java/com/spotify/reaper/resources/view/RepairScheduleStatusTest.java +++ b/src/server/src/test/java/com/spotify/reaper/resources/view/RepairScheduleStatusTest.java @@ -1,13 +1,25 @@ -package com.spotify.reaper.resources.view; +/* + * 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. + */ -import com.datastax.driver.core.utils.UUIDs; -import com.google.common.collect.Lists; +package com.spotify.reaper.resources.view; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.spotify.reaper.SimpleReaperClient; import com.spotify.reaper.core.RepairSchedule; +import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; import org.apache.cassandra.repair.RepairParallelism; import org.joda.time.DateTime; import org.junit.Test; @@ -16,7 +28,7 @@ import static org.junit.Assert.assertEquals; -public class RepairScheduleStatusTest { +public final class RepairScheduleStatusTest { private static final Logger LOG = LoggerFactory.getLogger(RepairScheduleStatusTest.class); diff --git a/src/server/src/test/java/com/spotify/reaper/unit/cassandra/JmxProxyTest.java b/src/server/src/test/java/com/spotify/reaper/unit/cassandra/JmxProxyTest.java index 9caa1ad41..e68de5421 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/cassandra/JmxProxyTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/cassandra/JmxProxyTest.java @@ -11,17 +11,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.unit.cassandra; import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxConnectionFactory; import com.spotify.reaper.cassandra.JmxProxy; import org.junit.Test; import static org.junit.Assert.assertEquals; -public class JmxProxyTest { +public final class JmxProxyTest { @Test public void testVersionCompare() throws ReaperException { @@ -33,8 +33,10 @@ public void testVersionCompare() throws ReaperException { assertEquals(Integer.valueOf(1), JmxProxy.versionCompare("99.0.0", "9.0")); assertEquals(Integer.valueOf(1), JmxProxy.versionCompare("99.0.10", "99.0.1")); assertEquals(Integer.valueOf(-1), JmxProxy.versionCompare("99.0.10~1", "99.0.10~2")); - assertEquals(Integer.valueOf(0), JmxProxy.versionCompare("1.2.18-1~1.2.15.219.gec18fb4.9", - "1.2.18-1~1.2.15.219.gec17fb4.10")); + + assertEquals( + Integer.valueOf(0), + JmxProxy.versionCompare("1.2.18-1~1.2.15.219.gec18fb4.9", "1.2.18-1~1.2.15.219.gec17fb4.10")); } } diff --git a/src/server/src/test/java/com/spotify/reaper/unit/core/ClusterTest.java b/src/server/src/test/java/com/spotify/reaper/unit/core/ClusterTest.java index 42fa5fc8b..0e95ec5c3 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/core/ClusterTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/core/ClusterTest.java @@ -11,15 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.unit.core; - import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.Cluster; - import org.junit.Test; +import org.junit.Test; - import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertEquals; -public class ClusterTest { +public final class ClusterTest { @Test public void testGetSymbolicName() { diff --git a/src/server/src/test/java/com/spotify/reaper/unit/resources/ClusterResourceTest.java b/src/server/src/test/java/com/spotify/reaper/unit/resources/ClusterResourceTest.java index 13fa7b4b9..0f808d225 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/resources/ClusterResourceTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/resources/ClusterResourceTest.java @@ -1,8 +1,19 @@ +/* + * 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.spotify.reaper.unit.resources; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import com.spotify.reaper.AppContext; import com.spotify.reaper.ReaperException; import com.spotify.reaper.cassandra.JmxConnectionFactory; @@ -12,16 +23,19 @@ import com.spotify.reaper.resources.ClusterResource; import com.spotify.reaper.storage.MemoryStorage; import com.spotify.reaper.unit.service.TestRepairConfiguration; -import org.junit.Before; -import org.junit.Test; import java.net.URI; import java.time.Duration; import java.util.Arrays; - import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.Test; + import static org.fest.assertions.api.Assertions.assertThat; import static org.hibernate.validator.internal.util.Contracts.assertNotNull; import static org.junit.Assert.assertEquals; @@ -30,7 +44,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ClusterResourceTest { +public final class ClusterResourceTest { static final String CLUSTER_NAME = "testcluster"; static final String PARTITIONER = "org.apache.cassandra.dht.RandomPartitioner"; @@ -41,7 +55,6 @@ public class ClusterResourceTest { @Before public void setUp() throws Exception { - } @Test @@ -110,8 +123,7 @@ public void testModifyClusterSeeds() throws ReaperException { clusterResource.addCluster(mocks.uriInfo, Optional.of(SEED_HOST)); doReturn(Arrays.asList(SEED_HOST + 1)).when(mocks.jmxProxy).getLiveNodes(); - Response response = - clusterResource.modifyClusterSeed(mocks.uriInfo, CLUSTER_NAME, Optional.of(SEED_HOST + 1)); + Response response = clusterResource.modifyClusterSeed(mocks.uriInfo, CLUSTER_NAME, Optional.of(SEED_HOST + 1)); assertEquals(200, response.getStatus()); assertEquals(1, mocks.context.storage.getClusters().size()); @@ -120,9 +132,7 @@ public void testModifyClusterSeeds() throws ReaperException { assertEquals(1, cluster.getSeedHosts().size()); assertEquals(SEED_HOST + 1, cluster.getSeedHosts().iterator().next()); - - response = - clusterResource.modifyClusterSeed(mocks.uriInfo, CLUSTER_NAME, Optional.of(SEED_HOST + 1)); + response = clusterResource.modifyClusterSeed(mocks.uriInfo, CLUSTER_NAME, Optional.of(SEED_HOST + 1)); assertEquals(304, response.getStatus()); //when(mocks.jmxProxy.getLiveNodes()).thenReturn(Arrays.asList(SEED_HOST)); } @@ -136,14 +146,13 @@ public void addingAClusterAutomaticallySetupSchedulingRepairsWhenEnabled() throw when(mocks.jmxProxy.getTableNamesForKeyspace("keyspace1")) .thenReturn(Sets.newHashSet("table1")); - mocks.context.config = - TestRepairConfiguration.defaultConfigBuilder() - .withAutoScheduling( - TestRepairConfiguration.defaultAutoSchedulingConfigBuilder() - .thatIsEnabled() - .withTimeBeforeFirstSchedule(Duration.ofMinutes(1)) - .build()) - .build(); + mocks.context.config = TestRepairConfiguration.defaultConfigBuilder() + .withAutoScheduling( + TestRepairConfiguration.defaultAutoSchedulingConfigBuilder() + .thatIsEnabled() + .withTimeBeforeFirstSchedule(Duration.ofMinutes(1)) + .build()) + .build(); ClusterResource clusterResource = new ClusterResource(mocks.context); Response response = clusterResource.addCluster(mocks.uriInfo, Optional.of(SEED_HOST)); @@ -151,24 +160,21 @@ public void addingAClusterAutomaticallySetupSchedulingRepairsWhenEnabled() throw assertEquals(201, response.getStatus()); assertThat(mocks.context.storage.getAllRepairSchedules()).hasSize(1); assertThat( - mocks.context.storage.getRepairSchedulesForClusterAndKeyspace( - CLUSTER_NAME, "keyspace1")) + mocks.context.storage.getRepairSchedulesForClusterAndKeyspace( + CLUSTER_NAME, "keyspace1")) .hasSize(1); } private MockObjects initMocks() throws ReaperException { AppContext context = new AppContext(); - UriInfo uriInfo; - JmxProxy jmxProxy; - context.storage = new MemoryStorage(); context.config = TestRepairConfiguration.defaultConfig(); - uriInfo = mock(UriInfo.class); + UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getAbsolutePath()).thenReturn(SAMPLE_URI); when(uriInfo.getBaseUri()).thenReturn(SAMPLE_URI); - jmxProxy = mock(JmxProxy.class); + JmxProxy jmxProxy = mock(JmxProxy.class); when(jmxProxy.getClusterName()).thenReturn(CLUSTER_NAME); when(jmxProxy.getPartitioner()).thenReturn(PARTITIONER); @@ -181,23 +187,21 @@ public JmxProxy connect(Optional handler, String host, int }; return new MockObjects(context, uriInfo, jmxProxy); - } private static final class MockObjects { - public final AppContext context; - public final UriInfo uriInfo; - public final JmxProxy jmxProxy; - public MockObjects(AppContext context, UriInfo uriInfo, JmxProxy jmxProxy) { + final AppContext context; + final UriInfo uriInfo; + final JmxProxy jmxProxy; + + MockObjects(AppContext context, UriInfo uriInfo, JmxProxy jmxProxy) { super(); this.context = context; this.uriInfo = uriInfo; this.jmxProxy = jmxProxy; } - - } } diff --git a/src/server/src/test/java/com/spotify/reaper/unit/resources/RepairRunResourceTest.java b/src/server/src/test/java/com/spotify/reaper/unit/resources/RepairRunResourceTest.java index a3e4c2da0..58bdbe733 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/resources/RepairRunResourceTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/resources/RepairRunResourceTest.java @@ -1,14 +1,36 @@ +/* + * 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.spotify.reaper.unit.resources; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyCollectionOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperApplicationConfiguration; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxConnectionFactory; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.cassandra.RepairStatusHandler; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.resources.RepairRunResource; +import com.spotify.reaper.resources.view.RepairRunStatus; +import com.spotify.reaper.service.RepairManager; +import com.spotify.reaper.service.RingRange; +import com.spotify.reaper.service.SegmentRunner; +import com.spotify.reaper.storage.MemoryStorage; +import com.spotify.reaper.unit.service.RepairRunnerTest; import java.math.BigInteger; import java.net.URI; @@ -18,68 +40,63 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; - import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import com.datastax.driver.core.utils.UUIDs; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import org.apache.cassandra.repair.RepairParallelism; import org.assertj.core.util.Maps; import org.joda.time.DateTimeUtils; import org.junit.Before; import org.junit.Test; -import com.datastax.driver.core.utils.UUIDs; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplicationConfiguration; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxConnectionFactory; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.cassandra.RepairStatusHandler; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.resources.RepairRunResource; -import com.spotify.reaper.resources.view.RepairRunStatus; -import com.spotify.reaper.service.RepairManager; -import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.service.SegmentRunner; -import com.spotify.reaper.storage.MemoryStorage; -import com.spotify.reaper.unit.service.RepairRunnerTest; -public class RepairRunResourceTest { - - static final String CLUSTER_NAME = "testcluster"; - static final String PARTITIONER = "org.apache.cassandra.dht.RandomPartitioner"; - static final String SEED_HOST = "TestHost"; - static final String KEYSPACE = "testKeyspace"; - static final Boolean INCREMENTAL = false; - static final Set TABLES = Sets.newHashSet("testTable"); - static final Set NODES = Collections.emptySet(); - static final Set DATACENTERS = Collections.emptySet(); - static final Map NODES_MAP = Maps.newHashMap("node1", "127.0.0.1"); - static final String OWNER = "test"; - int THREAD_CNT = 1; - int REPAIR_TIMEOUT_S = 60; - int RETRY_DELAY_S = 10; - long TIME_CREATE = 42l; - long TIME_START = 43l; - URI SAMPLE_URI = URI.create("http://test"); - int SEGMENT_CNT = 6; - double REPAIR_INTENSITY = 0.5f; - RepairParallelism REPAIR_PARALLELISM = RepairParallelism.SEQUENTIAL; - List TOKENS = Lists.newArrayList(BigInteger.valueOf(0l), BigInteger.valueOf(100l), - BigInteger.valueOf(200l)); +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyCollectionOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class RepairRunResourceTest { + + private static final String CLUSTER_NAME = "testcluster"; + private static final String PARTITIONER = "org.apache.cassandra.dht.RandomPartitioner"; + private static final String SEED_HOST = "TestHost"; + private static final String KEYSPACE = "testKeyspace"; + private static final Boolean INCREMENTAL = false; + private static final Set TABLES = Sets.newHashSet("testTable"); + private static final Set NODES = Collections.emptySet(); + private static final Set DATACENTERS = Collections.emptySet(); + private static final Map NODES_MAP = Maps.newHashMap("node1", "127.0.0.1"); + private static final String OWNER = "test"; + private static final int THREAD_CNT = 1; + private static final int REPAIR_TIMEOUT_S = 60; + private static final int RETRY_DELAY_S = 10; + private static final long TIME_CREATE = 42L; + private static final long TIME_START = 43L; + private static final URI SAMPLE_URI = URI.create("http://test"); + private static final int SEGMENT_CNT = 6; + private static final double REPAIR_INTENSITY = 0.5f; + private static final RepairParallelism REPAIR_PARALLELISM = RepairParallelism.SEQUENTIAL; + + private static final List TOKENS = Lists.newArrayList( + BigInteger.valueOf(0L), + BigInteger.valueOf(100L), + BigInteger.valueOf(200L)); AppContext context; UriInfo uriInfo; @Before public void setUp() throws Exception { - SegmentRunner.segmentRunners.clear(); + SegmentRunner.SEGMENT_RUNNERS.clear(); context = new AppContext(); context.repairManager = new RepairManager(); @@ -108,7 +125,7 @@ public void setUp() throws Exception { when(proxy.getRangeToEndpointMap(anyString())).thenReturn(RepairRunnerTest.sixNodeCluster()); when(proxy.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), any(RepairParallelism.class), anyCollectionOf(String.class), anyBoolean(), anyCollectionOf(String.class))) - .thenReturn(1); + .thenReturn(1); context.jmxConnectionFactory = new JmxConnectionFactory() { @Override @@ -118,8 +135,9 @@ public JmxProxy connect(Optional handler, String host, int } }; - RepairUnit.Builder repairUnitBuilder = new RepairUnit.Builder(CLUSTER_NAME, KEYSPACE, TABLES, INCREMENTAL, NODES, - DATACENTERS); + RepairUnit.Builder repairUnitBuilder = new RepairUnit + .Builder(CLUSTER_NAME, KEYSPACE, TABLES, INCREMENTAL, NODES, DATACENTERS); + context.storage.addRepairUnit(repairUnitBuilder); } @@ -127,23 +145,27 @@ private Response addDefaultRepairRun(RepairRunResource resource) { return addRepairRun(resource, uriInfo, CLUSTER_NAME, KEYSPACE, TABLES, OWNER, "", SEGMENT_CNT, NODES); } - private Response addRepairRun(RepairRunResource resource, UriInfo uriInfo, - String clusterName, String keyspace, Set columnFamilies, - String owner, String cause, Integer segments, Set nodes) { - return resource.addRepairRun(uriInfo, - clusterName == null ? Optional.absent() - : Optional.of(clusterName), - keyspace == null ? Optional.absent() - : Optional.of(keyspace), - columnFamilies == null ? Optional.absent() - : Optional - .of(columnFamilies.iterator().next()), - owner == null ? Optional.absent() : Optional.of(owner), - cause == null ? Optional.absent() : Optional.of(cause), - segments == null ? Optional.absent() - : Optional.of(segments), - Optional.of(REPAIR_PARALLELISM.name()), - Optional.absent(), + private Response addRepairRun( + RepairRunResource resource, + UriInfo uriInfo, + String clusterName, + String keyspace, + Set columnFamilies, + String owner, + String cause, + Integer segments, + Set nodes) { + + return resource.addRepairRun( + uriInfo, + Optional.fromNullable(clusterName), + Optional.fromNullable(keyspace), + columnFamilies == null ? Optional.absent() : Optional .of(columnFamilies.iterator().next()), + Optional.fromNullable(owner), + Optional.fromNullable(cause), + Optional.fromNullable(segments), + Optional.of(REPAIR_PARALLELISM.name()), + Optional.absent(), Optional.absent(), nodes == null || nodes.isEmpty() ? Optional.absent() : Optional.of(nodes.iterator().next()), Optional.absent()); @@ -197,10 +219,13 @@ public void testTriggerNotExistingRun() throws ReaperException { @Test public void testTriggerAlreadyRunningRun() throws InterruptedException, ReaperException { DateTimeUtils.setCurrentMillisFixed(TIME_CREATE); - context.repairManager.initializeThreadPool(THREAD_CNT, REPAIR_TIMEOUT_S, TimeUnit.SECONDS, - RETRY_DELAY_S, TimeUnit.SECONDS); + + context.repairManager + .initializeThreadPool(THREAD_CNT, REPAIR_TIMEOUT_S, TimeUnit.SECONDS, RETRY_DELAY_S, TimeUnit.SECONDS); + RepairRunResource resource = new RepairRunResource(context); Response response = addDefaultRepairRun(resource); + assertTrue(response.getEntity().toString(), response.getEntity() instanceof RepairRunStatus); RepairRunStatus repairRunStatus = (RepairRunStatus) response.getEntity(); UUID runId = repairRunStatus.getId(); @@ -216,9 +241,10 @@ public void testTriggerAlreadyRunningRun() throws InterruptedException, ReaperEx public void testTriggerNewRunAlreadyRunningRun() throws InterruptedException, ReaperException { DateTimeUtils.setCurrentMillisFixed(TIME_CREATE); context.repairManager.initializeThreadPool(THREAD_CNT, REPAIR_TIMEOUT_S, TimeUnit.SECONDS, - RETRY_DELAY_S, TimeUnit.SECONDS); + RETRY_DELAY_S, TimeUnit.SECONDS); RepairRunResource resource = new RepairRunResource(context); Response response = addDefaultRepairRun(resource); + assertTrue(response.getEntity().toString(), response.getEntity() instanceof RepairRunStatus); RepairRunStatus repairRunStatus = (RepairRunStatus) response.getEntity(); UUID runId = repairRunStatus.getId(); @@ -241,7 +267,6 @@ public void testTriggerNewRunAlreadyRunningRun() throws InterruptedException, Re // We expect it to fail as we cannot have 2 running runs for the same repair unit at once assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); - } @Test @@ -265,7 +290,7 @@ public void testAddRunMissingArgument() { @Test public void testTriggerRunMissingArgument() { context.repairManager.initializeThreadPool(THREAD_CNT, REPAIR_TIMEOUT_S, TimeUnit.SECONDS, - RETRY_DELAY_S, TimeUnit.SECONDS); + RETRY_DELAY_S, TimeUnit.SECONDS); RepairRunResource resource = new RepairRunResource(context); Response response = addRepairRun(resource, uriInfo, CLUSTER_NAME, null, TABLES, OWNER, null, SEGMENT_CNT, NODES); @@ -277,14 +302,15 @@ public void testTriggerRunMissingArgument() { public void testPauseNotRunningRun() throws InterruptedException, ReaperException { DateTimeUtils.setCurrentMillisFixed(TIME_CREATE); context.repairManager.initializeThreadPool(THREAD_CNT, REPAIR_TIMEOUT_S, TimeUnit.SECONDS, - RETRY_DELAY_S, TimeUnit.SECONDS); + RETRY_DELAY_S, TimeUnit.SECONDS); RepairRunResource resource = new RepairRunResource(context); Response response = addDefaultRepairRun(resource); + assertTrue(response.getEntity().toString(), response.getEntity() instanceof RepairRunStatus); RepairRunStatus repairRunStatus = (RepairRunStatus) response.getEntity(); UUID runId = repairRunStatus.getId(); response = resource.modifyRunState(uriInfo, runId, - Optional.of(RepairRun.RunState.PAUSED.toString())); + Optional.of(RepairRun.RunState.PAUSED.toString())); Thread.sleep(200); assertEquals(405, response.getStatus()); @@ -293,15 +319,15 @@ public void testPauseNotRunningRun() throws InterruptedException, ReaperExceptio assertEquals(RepairRun.RunState.NOT_STARTED, repairRun.getRunState()); // but the running segment should be untouched assertEquals(0, - context.storage.getSegmentAmountForRepairRunWithState(runId, - RepairSegment.State.RUNNING)); + context.storage.getSegmentAmountForRepairRunWithState(runId, + RepairSegment.State.RUNNING)); } @Test public void testPauseNotExistingRun() throws InterruptedException, ReaperException { RepairRunResource resource = new RepairRunResource(context); Response response = resource.modifyRunState(uriInfo, UUIDs.timeBased(), - Optional.of(RepairRun.RunState.PAUSED.toString())); + Optional.of(RepairRun.RunState.PAUSED.toString())); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); assertEquals(0, context.storage.getRepairRunsWithState(RepairRun.RunState.RUNNING).size()); } diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/AutoSchedulingManagerTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/AutoSchedulingManagerTest.java index ad36f6f32..4554bfd22 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/AutoSchedulingManagerTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/AutoSchedulingManagerTest.java @@ -1,3 +1,17 @@ +/* + * 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.spotify.reaper.unit.service; import com.spotify.reaper.AppContext; @@ -6,14 +20,17 @@ import com.spotify.reaper.service.AutoSchedulingManager; import com.spotify.reaper.service.ClusterRepairScheduler; import com.spotify.reaper.storage.MemoryStorage; -import org.junit.Before; -import org.junit.Test; import java.util.Collections; -import static org.mockito.Mockito.*; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; -public class AutoSchedulingManagerTest { +public final class AutoSchedulingManagerTest { private static final Cluster CLUSTER_1 = new Cluster("cluster1", null, Collections.singleton(null)); private static final Cluster CLUSTER_2 = new Cluster("cluster2", null, Collections.singleton(null)); diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/ClusterRepairSchedulerTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/ClusterRepairSchedulerTest.java index 2478f1b46..aca478407 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/ClusterRepairSchedulerTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/ClusterRepairSchedulerTest.java @@ -1,22 +1,19 @@ -package com.spotify.reaper.unit.service; - -import static java.lang.String.format; -import static org.fest.assertions.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +/* + * 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. + */ -import java.time.Duration; -import java.util.Collection; -import java.util.Collections; - -import org.apache.cassandra.repair.RepairParallelism; -import org.joda.time.DateTime; -import org.junit.Before; -import org.junit.Test; +package com.spotify.reaper.unit.service; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import com.spotify.reaper.AppContext; import com.spotify.reaper.ReaperApplicationConfiguration; import com.spotify.reaper.ReaperException; @@ -28,7 +25,25 @@ import com.spotify.reaper.service.ClusterRepairScheduler; import com.spotify.reaper.storage.MemoryStorage; -public class ClusterRepairSchedulerTest { +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.cassandra.repair.RepairParallelism; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; + +import static java.lang.String.format; +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class ClusterRepairSchedulerTest { private static final Cluster CLUSTER = new Cluster("cluster1", null, Collections.singleton("127.0.0.1")); private static final DateTime TWO_HOURS_AGO = DateTime.now().minusHours(2); @@ -50,7 +65,16 @@ public void setup() throws ReaperException { context.jmxConnectionFactory = mock(JmxConnectionFactory.class); clusterRepairAuto = new ClusterRepairScheduler(context); jmxProxy = mock(JmxProxy.class); - when(context.jmxConnectionFactory.connectAny(CLUSTER, context.config.getJmxConnectionTimeoutInSeconds())).thenReturn(jmxProxy); + + when(context.jmxConnectionFactory.connectAny(CLUSTER, context.config.getJmxConnectionTimeoutInSeconds())) + .thenReturn(jmxProxy); + + when( + context.jmxConnectionFactory.connectAny( + Optional.absent(), + CLUSTER.getSeedHosts(), + context.config.getJmxConnectionTimeoutInSeconds())) + .thenReturn(jmxProxy); } @Test @@ -153,9 +177,14 @@ public void spreadsKeyspaceScheduling() throws Exception { clusterRepairAuto.scheduleRepairs(CLUSTER); assertThatClusterRepairSchedules(context.storage.getRepairSchedulesForCluster(CLUSTER.getName())) .hasScheduleCount(3) - .repairScheduleForKeyspace("keyspace1").hasNextActivationDateCloseTo(timeOfFirstSchedule()).andThen() - .repairScheduleForKeyspace("keyspace2").hasNextActivationDateCloseTo(timeOfFirstSchedule().plusHours(6)).andThen() - .repairScheduleForKeyspace("keyspace4").hasNextActivationDateCloseTo(timeOfFirstSchedule().plusHours(12)); + .repairScheduleForKeyspace("keyspace1") + .hasNextActivationDateCloseTo(timeOfFirstSchedule()) + .andThen() + .repairScheduleForKeyspace("keyspace2") + .hasNextActivationDateCloseTo(timeOfFirstSchedule().plusHours(6)) + .andThen() + .repairScheduleForKeyspace("keyspace4") + .hasNextActivationDateCloseTo(timeOfFirstSchedule().plusHours(12)); } private DateTime timeOfFirstSchedule() { @@ -187,34 +216,38 @@ private ClusterRepairScheduleAssertion assertThatClusterRepairSchedules(Collecti } private class ClusterRepairScheduleAssertion { + private final Collection repairSchedules; - public ClusterRepairScheduleAssertion(Collection repairSchedules) { + ClusterRepairScheduleAssertion(Collection repairSchedules) { this.repairSchedules = repairSchedules; } - public ClusterRepairScheduleAssertion hasScheduleCount(int size) { + ClusterRepairScheduleAssertion hasScheduleCount(int size) { assertThat(size).isEqualTo(size); return this; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion repairScheduleForKeyspace(String keyspace) { + ClusterRepairScheduleAssertion.RepairScheduleAssertion repairScheduleForKeyspace(String keyspace) { RepairSchedule keyspaceRepairSchedule = repairSchedules.stream() - .filter(repairSchedule -> context.storage.getRepairUnit(repairSchedule.getRepairUnitId()).get().getKeyspaceName().equals(keyspace)) + .filter(repairSchedule + -> context.storage + .getRepairUnit(repairSchedule.getRepairUnitId()).get().getKeyspaceName().equals(keyspace)) .findFirst() .orElseThrow(() -> new AssertionError(format("No repair schedule found for keyspace %s", keyspace))); return new ClusterRepairScheduleAssertion.RepairScheduleAssertion(keyspaceRepairSchedule); } - public class RepairScheduleAssertion { + private final class RepairScheduleAssertion { private final RepairSchedule repairSchedule; - public RepairScheduleAssertion(RepairSchedule repairSchedule) { + RepairScheduleAssertion(RepairSchedule repairSchedule) { this.repairSchedule = repairSchedule; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasSameConfigItemsAs(ReaperApplicationConfiguration config) { + RepairScheduleAssertion hasSameConfigItemsAs(ReaperApplicationConfiguration config) { + assertThat(repairSchedule.getDaysBetween()).isEqualTo(config.getScheduleDaysBetween()); assertThat(repairSchedule.getIntensity()).isEqualTo(config.getRepairIntensity()); assertThat(repairSchedule.getSegmentCount()).isEqualTo(config.getSegmentCount()); @@ -222,29 +255,37 @@ public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasSameConfigItems return this; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasNextActivationDateCloseTo(DateTime dateTime) { - assertThat(repairSchedule.getNextActivation().toDate()).isCloseTo(dateTime.toDate(), Duration.ofSeconds(10).toMillis()); + RepairScheduleAssertion hasNextActivationDateCloseTo(DateTime dateTime) { + + assertThat( + repairSchedule.getNextActivation().toDate()).isCloseTo(dateTime.toDate(), + Duration.ofSeconds(10).toMillis()); + return this; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasOwner(String owner) { + RepairScheduleAssertion hasOwner(String owner) { assertThat(repairSchedule.getOwner()).isEqualTo(owner); return this; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasCreationTimeCloseTo(DateTime dateTime) { - assertThat(repairSchedule.getCreationTime().toDate()).isCloseTo(dateTime.toDate(), Duration.ofSeconds(10).toMillis()); + RepairScheduleAssertion hasCreationTimeCloseTo(DateTime dateTime) { + + assertThat( + repairSchedule.getCreationTime().toDate()).isCloseTo(dateTime.toDate(), + Duration.ofSeconds(10).toMillis()); + return this; } - public ClusterRepairScheduleAssertion.RepairScheduleAssertion hasCreationTime(DateTime dateTime) { + RepairScheduleAssertion hasCreationTime(DateTime dateTime) { assertThat(repairSchedule.getCreationTime()).isEqualTo(dateTime); return this; } - public ClusterRepairScheduleAssertion andThen() { + ClusterRepairScheduleAssertion andThen() { return ClusterRepairScheduleAssertion.this; } } } -} \ No newline at end of file +} diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/RepairRunnerTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/RepairRunnerTest.java index 472f4a40d..ae2ddab11 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/RepairRunnerTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/RepairRunnerTest.java @@ -11,17 +11,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.unit.service; -import static org.awaitility.Awaitility.await; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyCollectionOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperApplicationConfiguration; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxConnectionFactory; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.cassandra.RepairStatusHandler; +import com.spotify.reaper.core.Cluster; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.service.RepairManager; +import com.spotify.reaper.service.RepairRunner; +import com.spotify.reaper.service.RingRange; +import com.spotify.reaper.service.SegmentGenerator; +import com.spotify.reaper.service.SegmentRunner; +import com.spotify.reaper.storage.IStorage; +import com.spotify.reaper.storage.MemoryStorage; import java.math.BigInteger; import java.util.Collections; @@ -33,6 +42,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.utils.progress.ProgressEventType; @@ -40,38 +53,25 @@ import org.joda.time.DateTimeUtils; import org.junit.Before; import org.junit.Test; -import org.mockito.Matchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplicationConfiguration; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxConnectionFactory; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.cassandra.RepairStatusHandler; -import com.spotify.reaper.core.Cluster; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.service.RepairManager; -import com.spotify.reaper.service.RepairRunner; -import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.service.SegmentGenerator; -import com.spotify.reaper.service.SegmentRunner; -import com.spotify.reaper.storage.IStorage; -import com.spotify.reaper.storage.MemoryStorage; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public final class RepairRunnerTest { + private static final Logger LOG = LoggerFactory.getLogger(RepairRunnerTest.class); @Before public void setUp() throws Exception { - SegmentRunner.segmentRunners.clear(); + SegmentRunner.SEGMENT_RUNNERS.clear(); } @Test @@ -82,7 +82,7 @@ public void testHangingRepair() throws InterruptedException, ReaperException { final boolean INCREMENTAL_REPAIR = false; final Set NODES = Sets.newHashSet("127.0.0.1"); final Set DATACENTERS = Collections.emptySet(); - final long TIME_RUN = 41l; + final long TIME_RUN = 41L; final double INTENSITY = 0.5f; final IStorage storage = new MemoryStorage(); @@ -113,6 +113,7 @@ public void testHangingRepair() throws InterruptedException, ReaperException { @Override public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + final JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn(CLUSTER_NAME); when(jmx.isConnectionAlive()).thenReturn(true); @@ -121,66 +122,98 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter()).thenReturn("dc1"); when(jmx.getDataCenter(anyString())).thenReturn("dc1"); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), Matchers.any(), - Sets.newHashSet(anyString()), anyBoolean(), anyCollectionOf(String.class))).then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - - final int repairNumber = repairAttempts.getAndIncrement(); - switch (repairNumber) { - case 1: - new Thread() { - @Override - public void run() { - handler.get() - .handle(repairNumber, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), null); - assertEquals(RepairSegment.State.RUNNING, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - } - }.start(); - break; - case 2: - new Thread() { - @Override - public void run() { - handler.get() - .handle(repairNumber, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), null); - assertEquals(RepairSegment.State.RUNNING, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - handler.get() - .handle(repairNumber, Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), Optional.absent(), null); - assertEquals(RepairSegment.State.DONE, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - handler.get() - .handle(repairNumber, Optional.of(ActiveRepairService.Status.FINISHED), Optional.absent(), null); - mutex.release(); - LOG.info("MUTEX RELEASED"); - } - }.start(); - break; - default: - fail("triggerRepair should only have been called twice"); - } - LOG.info("repair number : " + repairNumber); - return repairNumber; - }); + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) + .then((invocation) -> { + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + + final int repairNumber = repairAttempts.getAndIncrement(); + switch (repairNumber) { + case 1: + new Thread() { + @Override + public void run() { + handler.get().handle( + repairNumber, + Optional.of(ActiveRepairService.Status.STARTED), + Optional.absent(), + null); + + assertEquals( + RepairSegment.State.RUNNING, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + } + }.start(); + break; + case 2: + new Thread() { + @Override + public void run() { + + handler.get().handle( + repairNumber, + Optional.of(ActiveRepairService.Status.STARTED), + Optional.absent(), + null); + + assertEquals( + RepairSegment.State.RUNNING, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + + handler.get().handle( + repairNumber, + Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), + Optional.absent(), + null); + + assertEquals( + RepairSegment.State.DONE, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + + handler.get().handle( + repairNumber, + Optional.of(ActiveRepairService.Status.FINISHED), + Optional.absent(), + null); + + mutex.release(); + LOG.info("MUTEX RELEASED"); + } + }.start(); + break; + default: + fail("triggerRepair should only have been called twice"); + } + LOG.info("repair number : " + repairNumber); + return repairNumber; + }); return jmx; } }; context.repairManager.startRepairRun(context, run); await().with().atMost(20, TimeUnit.SECONDS).until(() - -> { - try { - mutex.acquire(); - LOG.info("MUTEX ACQUIRED"); - // TODO: refactor so that we can properly wait for the repair runner to finish rather than - // TODO: using this sleep(). - Thread.sleep(1000); - return true; - } catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - }); + -> { + try { + mutex.acquire(); + LOG.info("MUTEX ACQUIRED"); + // TODO: refactor so that we can properly wait for the repair runner to finish rather than + // TODO: using this sleep(). + Thread.sleep(1000); + return true; + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + }); assertEquals(RepairRun.RunState.DONE, storage.getRepairRun(RUN_ID).get().getRunState()); } @@ -192,7 +225,7 @@ public void testHangingRepairNewAPI() throws InterruptedException, ReaperExcepti final boolean INCREMENTAL_REPAIR = false; final Set NODES = Sets.newHashSet("127.0.0.1"); final Set DATACENTERS = Collections.emptySet(); - final long TIME_RUN = 41l; + final long TIME_RUN = 41L; final double INTENSITY = 0.5f; final IStorage storage = new MemoryStorage(); @@ -223,6 +256,7 @@ public void testHangingRepairNewAPI() throws InterruptedException, ReaperExcepti @Override public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + final JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn(CLUSTER_NAME); when(jmx.isConnectionAlive()).thenReturn(true); @@ -232,65 +266,74 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter(anyString())).thenReturn("dc1"); //doNothing().when(jmx).cancelAllRepairs(); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), - Matchers.any(), - Sets.newHashSet(anyString()), anyBoolean(), anyCollectionOf(String.class))).then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - - final int repairNumber = repairAttempts.getAndIncrement(); - switch (repairNumber) { - case 1: - new Thread() { - @Override - public void run() { - handler.get().handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.START), null); - assertEquals(RepairSegment.State.RUNNING, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - } - }.start(); - break; - case 2: - new Thread() { - @Override - public void run() { - handler.get() - .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.START), null); - assertEquals(RepairSegment.State.RUNNING, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - handler.get() - .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.SUCCESS), null); - assertEquals(RepairSegment.State.DONE, - storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - handler.get() - .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.COMPLETE), null); - mutex.release(); - LOG.info("MUTEX RELEASED"); - } - }.start(); - break; - default: - fail("triggerRepair should only have been called twice"); - } - return repairNumber; - }); + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) + .then((invocation) -> { + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + + final int repairNumber = repairAttempts.getAndIncrement(); + switch (repairNumber) { + case 1: + new Thread() { + @Override + public void run() { + handler.get().handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.START), null); + assertEquals(RepairSegment.State.RUNNING, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + } + }.start(); + break; + case 2: + new Thread() { + @Override + public void run() { + handler.get() + .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.START), null); + assertEquals(RepairSegment.State.RUNNING, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + handler.get() + .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.SUCCESS), null); + assertEquals(RepairSegment.State.DONE, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + handler.get() + .handle(repairNumber, Optional.absent(), Optional.of(ProgressEventType.COMPLETE), null); + mutex.release(); + LOG.info("MUTEX RELEASED"); + } + }.start(); + break; + default: + fail("triggerRepair should only have been called twice"); + } + return repairNumber; + }); return jmx; } }; context.repairManager.startRepairRun(context, run); await().with().atMost(20, TimeUnit.SECONDS).until(() - -> { - try { - mutex.acquire(); - LOG.info("MUTEX ACQUIRED"); - // TODO: refactor so that we can properly wait for the repair runner to finish rather than - // TODO: using this sleep(). - Thread.sleep(1000); - return true; - } catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - }); + -> { + try { + mutex.acquire(); + LOG.info("MUTEX ACQUIRED"); + // TODO: refactor so that we can properly wait for the repair runner to finish rather than + // TODO: using this sleep(). + Thread.sleep(1000); + return true; + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + }); assertEquals(RepairRun.RunState.DONE, storage.getRepairRun(RUN_ID).get().getRunState()); } @@ -302,7 +345,7 @@ public void testResumeRepair() throws InterruptedException, ReaperException { final boolean INCREMENTAL_REPAIR = false; final Set NODES = Sets.newHashSet("127.0.0.1"); final Set DATACENTERS = Collections.emptySet(); - final long TIME_RUN = 41l; + final long TIME_RUN = 41L; final double INTENSITY = 0.5f; final IStorage storage = new MemoryStorage(); @@ -334,6 +377,7 @@ public void testResumeRepair() throws InterruptedException, ReaperException { @Override public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + final JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn(CLUSTER_NAME); when(jmx.isConnectionAlive()).thenReturn(true); @@ -342,19 +386,35 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter()).thenReturn("dc1"); when(jmx.getDataCenter(anyString())).thenReturn("dc1"); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), Matchers.any(), - Sets.newHashSet(anyString()), anyBoolean(), anyCollectionOf(String.class))).then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); - new Thread() { - @Override - public void run() { - handler.get().handle(1, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), null); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), Optional.absent(), null); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.FINISHED), Optional.absent(), null); - } - }.start(); - return 1; - }); + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) + .then((invocation) -> { + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(RUN_ID, SEGMENT_ID).get().getState()); + + new Thread() { + @Override + public void run() { + handler.get() + .handle(1, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), null); + + handler.get() + .handle(1, Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), Optional.absent(), null); + + handler.get() + .handle(1, Optional.of(ActiveRepairService.Status.FINISHED), Optional.absent(), null); + } + }.start(); + return 1; + }); return jmx; } }; @@ -382,7 +442,7 @@ public void getPossibleParallelRepairsTest() throws Exception { @Test public void getParallelSegmentsTest() throws ReaperException { List tokens = Lists - .transform(Lists.newArrayList("0", "50", "100", "150", "200", "250"), (s) -> new BigInteger(s)); + .transform(Lists.newArrayList("0", "50", "100", "150", "200", "250"), (string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator(new BigInteger("0"), new BigInteger("299")); List segments = generator.generateSegments(32, tokens, Boolean.FALSE); @@ -394,17 +454,17 @@ public void getParallelSegmentsTest() throws ReaperException { segments ); assertEquals(2, ranges.size()); - assertEquals( "0", ranges.get(0).getStart().toString()); + assertEquals("0", ranges.get(0).getStart().toString()); assertEquals("150", ranges.get(0).getEnd().toString()); assertEquals("150", ranges.get(1).getStart().toString()); - assertEquals( "0", ranges.get(1).getEnd().toString()); + assertEquals("0", ranges.get(1).getEnd().toString()); } @Test public void getParallelSegmentsTest2() throws ReaperException { List tokens = Lists.transform( - Lists.newArrayList("0", "25", "50", "75", "100", "125", "150", "175", "200", "225", "250"), - (s) -> new BigInteger(s)); + Lists.newArrayList("0", "25", "50", "75", "100", "125", "150", "175", "200", "225", "250"), + (string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator(new BigInteger("0"), new BigInteger("299")); List segments = generator.generateSegments(32, tokens, Boolean.FALSE); @@ -416,28 +476,28 @@ public void getParallelSegmentsTest2() throws ReaperException { segments ); assertEquals(2, ranges.size()); - assertEquals( "0", ranges.get(0).getStart().toString()); + assertEquals("0", ranges.get(0).getStart().toString()); assertEquals("150", ranges.get(0).getEnd().toString()); assertEquals("150", ranges.get(1).getStart().toString()); - assertEquals( "0", ranges.get(1).getEnd().toString()); + assertEquals("0", ranges.get(1).getEnd().toString()); } public static Map, List> threeNodeCluster() { Map, List> map = Maps.newHashMap(); - map = addRangeToMap(map, "0", "50", "a1", "a2", "a3"); - map = addRangeToMap(map, "50", "100", "a2", "a3", "a1"); - map = addRangeToMap(map, "100", "0", "a3", "a1", "a2"); + map = addRangeToMap(map, "0", "50", "a1", "a2", "a3"); + map = addRangeToMap(map, "50", "100", "a2", "a3", "a1"); + map = addRangeToMap(map, "100", "0", "a3", "a1", "a2"); return map; } public static Map, List> sixNodeCluster() { Map, List> map = Maps.newLinkedHashMap(); - map = addRangeToMap(map, "0", "50", "a1", "a2", "a3"); - map = addRangeToMap(map, "50", "100", "a2", "a3", "a4"); + map = addRangeToMap(map, "0", "50", "a1", "a2", "a3"); + map = addRangeToMap(map, "50", "100", "a2", "a3", "a4"); map = addRangeToMap(map, "100", "150", "a3", "a4", "a5"); map = addRangeToMap(map, "150", "200", "a4", "a5", "a6"); map = addRangeToMap(map, "200", "250", "a5", "a6", "a1"); - map = addRangeToMap(map, "250", "0", "a6", "a1", "a2"); + map = addRangeToMap(map, "250", "0", "a6", "a1", "a2"); return map; } @@ -460,9 +520,13 @@ public static Map sixNodeClusterEndpoint() { return map; } - private static Map, List> addRangeToMap(Map, List> map, - String rStart, String rEnd, String... hosts) { - List range = Lists.newArrayList(rStart, rEnd); + private static Map, List> addRangeToMap( + Map, List> map, + String start, + String end, + String... hosts) { + + List range = Lists.newArrayList(start, end); List endPoints = Lists.newArrayList(hosts); map.put(range, endPoints); return map; diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/RingRangeTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/RingRangeTest.java index 613259925..8a1564b98 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/RingRangeTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/RingRangeTest.java @@ -1,72 +1,86 @@ +/* + * 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.spotify.reaper.unit.service; -import com.google.common.collect.Lists; import com.spotify.reaper.service.RingRange; -import org.junit.Test; - import java.math.BigInteger; import java.util.List; +import com.google.common.collect.Lists; +import org.junit.Test; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class RingRangeTest { +public final class RingRangeTest { @Test public void testSpan() throws Exception { - RingRange r_0_20 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(20l)); - BigInteger ringSize_200 = BigInteger.valueOf(200l); + RingRange r_0_20 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(20L)); + BigInteger ringSize_200 = BigInteger.valueOf(200L); assertEquals(20, r_0_20.span(ringSize_200).intValue()); - RingRange r_20_0 = new RingRange(BigInteger.valueOf(20l), BigInteger.valueOf(0l)); + RingRange r_20_0 = new RingRange(BigInteger.valueOf(20L), BigInteger.valueOf(0L)); assertEquals(180, r_20_0.span(ringSize_200).intValue()); } @Test public void testEncloses() throws Exception { - RingRange r_0_20 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(20l)); + RingRange r_0_20 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(20L)); - RingRange r_5_15 = new RingRange(BigInteger.valueOf(5l), BigInteger.valueOf(15l)); + RingRange r_5_15 = new RingRange(BigInteger.valueOf(5L), BigInteger.valueOf(15L)); assertTrue(r_0_20.encloses(r_5_15)); - RingRange r_5_25 = new RingRange(BigInteger.valueOf(5l), BigInteger.valueOf(25l)); + RingRange r_5_25 = new RingRange(BigInteger.valueOf(5L), BigInteger.valueOf(25L)); assertFalse(r_0_20.encloses(r_5_25)); - RingRange r_190_25 = new RingRange(BigInteger.valueOf(190l), BigInteger.valueOf(25l)); + RingRange r_190_25 = new RingRange(BigInteger.valueOf(190L), BigInteger.valueOf(25L)); assertFalse(r_0_20.encloses(r_190_25)); - RingRange r_0_15 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(15l)); + RingRange r_0_15 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(15L)); assertTrue(r_0_20.encloses(r_0_15)); - RingRange r_190_15 = new RingRange(BigInteger.valueOf(190l), BigInteger.valueOf(15l)); + RingRange r_190_15 = new RingRange(BigInteger.valueOf(190L), BigInteger.valueOf(15L)); assertFalse(r_0_20.encloses(r_190_15)); - RingRange r_190_0 = new RingRange(BigInteger.valueOf(190l), BigInteger.valueOf(0l)); + RingRange r_190_0 = new RingRange(BigInteger.valueOf(190L), BigInteger.valueOf(0L)); assertFalse(r_0_20.encloses(r_190_0)); - RingRange r_15_20 = new RingRange(BigInteger.valueOf(15l), BigInteger.valueOf(20l)); + RingRange r_15_20 = new RingRange(BigInteger.valueOf(15L), BigInteger.valueOf(20L)); assertTrue(r_0_20.encloses(r_15_20)); assertTrue(r_0_20.encloses(r_0_20)); - RingRange r_20_25 = new RingRange(BigInteger.valueOf(20l), BigInteger.valueOf(25l)); + RingRange r_20_25 = new RingRange(BigInteger.valueOf(20L), BigInteger.valueOf(25L)); assertFalse(r_0_20.encloses(r_20_25)); - RingRange r_190_20 = new RingRange(BigInteger.valueOf(190l), BigInteger.valueOf(20l)); + RingRange r_190_20 = new RingRange(BigInteger.valueOf(190L), BigInteger.valueOf(20L)); assertFalse(r_190_20.encloses(r_5_25)); - RingRange r_200_10 = new RingRange(BigInteger.valueOf(200l), BigInteger.valueOf(10l)); + RingRange r_200_10 = new RingRange(BigInteger.valueOf(200L), BigInteger.valueOf(10L)); assertTrue(r_190_20.encloses(r_200_10)); - RingRange r_0_2 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(2l)); - RingRange r_5_190 = new RingRange(BigInteger.valueOf(5l), BigInteger.valueOf(190l)); + RingRange r_0_2 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(2L)); + RingRange r_5_190 = new RingRange(BigInteger.valueOf(5L), BigInteger.valueOf(190L)); assertFalse(r_0_2.encloses(r_5_190)); // 0_0 should enclose almost everything, but is not enclosed by anything - RingRange r_15_5 = new RingRange(BigInteger.valueOf(15l), BigInteger.valueOf(5l)); - RingRange r_0_0 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(0l)); + RingRange r_15_5 = new RingRange(BigInteger.valueOf(15L), BigInteger.valueOf(5L)); + RingRange r_0_0 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(0L)); assertTrue(r_0_0.encloses(r_0_20)); assertTrue(r_0_0.encloses(r_15_20)); // the exception is that 0_0 does not enclose 15_5 @@ -84,20 +98,20 @@ public void testEncloses() throws Exception { @Test public void isWrappingTest() { - RingRange r_0_0 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(0l)); + RingRange r_0_0 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(0L)); assertTrue(r_0_0.isWrapping()); - RingRange r_0_1 = new RingRange(BigInteger.valueOf(0l), BigInteger.valueOf(1l)); + RingRange r_0_1 = new RingRange(BigInteger.valueOf(0L), BigInteger.valueOf(1L)); assertFalse(r_0_1.isWrapping()); - RingRange r_1_0 = new RingRange(BigInteger.valueOf(1l), BigInteger.valueOf(0l)); + RingRange r_1_0 = new RingRange(BigInteger.valueOf(1L), BigInteger.valueOf(0L)); assertTrue(r_1_0.isWrapping()); } @Test public void mergeTest() { List ranges = Lists.newArrayList(); - ranges.add(new RingRange(BigInteger.valueOf(30l), BigInteger.valueOf(50l))); - ranges.add(new RingRange(BigInteger.valueOf(10l), BigInteger.valueOf(30l))); - ranges.add(new RingRange(BigInteger.valueOf(80l), BigInteger.valueOf(10l))); + ranges.add(new RingRange(BigInteger.valueOf(30L), BigInteger.valueOf(50L))); + ranges.add(new RingRange(BigInteger.valueOf(10L), BigInteger.valueOf(30L))); + ranges.add(new RingRange(BigInteger.valueOf(80L), BigInteger.valueOf(10L))); RingRange merged = RingRange.merge(ranges); assertEquals("80", merged.getStart().toString()); assertEquals("50", merged.getEnd().toString()); diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentGeneratorTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentGeneratorTest.java index 15645ab6a..09ae29c89 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentGeneratorTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentGeneratorTest.java @@ -11,75 +11,73 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.spotify.reaper.unit.service; -import com.google.common.base.Function; -import com.google.common.collect.Lists; +package com.spotify.reaper.unit.service; import com.spotify.reaper.ReaperException; import com.spotify.reaper.service.RingRange; import com.spotify.reaper.service.SegmentGenerator; -import org.junit.Test; - import java.math.BigInteger; import java.util.List; -import javax.annotation.Nullable; +import com.google.common.collect.Lists; +import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class SegmentGeneratorTest { +public final class SegmentGeneratorTest { @Test public void testGenerateSegments() throws Exception { - List tokens = Lists.transform( - Lists.newArrayList( - "0", "1", - "56713727820156410577229101238628035242", "56713727820156410577229101238628035243", - "113427455640312821154458202477256070484", "113427455640312821154458202477256070485"), - new Function() { - @Nullable - @Override - public BigInteger apply(String s) { - return new BigInteger(s); - } - } - ); + + List tokens = Lists.transform(Lists.newArrayList( + "0", + "1", + "56713727820156410577229101238628035242", + "56713727820156410577229101238628035243", + "113427455640312821154458202477256070484", + "113427455640312821154458202477256070485"), + (String string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator("foo.bar.RandomPartitioner"); List segments = generator.generateSegments(10, tokens, Boolean.FALSE); assertEquals(15, segments.size()); - assertEquals("(0,1]", - segments.get(0).toString()); - assertEquals("(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", - segments.get(5).toString()); + + assertEquals( + "(0,1]", + segments.get(0).toString()); + + assertEquals( + "(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", + segments.get(5).toString()); + assertEquals( "(113427455640312821154458202477256070484,113427455640312821154458202477256070485]", segments.get(10).toString()); - tokens = Lists.transform( - Lists.newArrayList( - "5", "6", - "56713727820156410577229101238628035242", "56713727820156410577229101238628035243", - "113427455640312821154458202477256070484", "113427455640312821154458202477256070485"), - new Function() { - @Nullable - @Override - public BigInteger apply(String s) { - return new BigInteger(s); - } - } - ); + tokens = Lists.transform(Lists.newArrayList( + "5", + "6", + "56713727820156410577229101238628035242", + "56713727820156410577229101238628035243", + "113427455640312821154458202477256070484", + "113427455640312821154458202477256070485"), + (String string) -> new BigInteger(string)); segments = generator.generateSegments(10, tokens, Boolean.FALSE); assertEquals(15, segments.size()); - assertEquals("(5,6]", - segments.get(0).toString()); - assertEquals("(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", - segments.get(5).toString()); + + assertEquals( + "(5,6]", + segments.get(0).toString()); + + assertEquals( + "(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", + segments.get(5).toString()); + assertEquals( "(113427455640312821154458202477256070484,113427455640312821154458202477256070485]", segments.get(10).toString()); @@ -87,17 +85,16 @@ public BigInteger apply(String s) { @Test(expected = ReaperException.class) public void testZeroSizeRange() throws Exception { + List tokenStrings = Lists.newArrayList( - "0", "1", - "56713727820156410577229101238628035242", "56713727820156410577229101238628035242", - "113427455640312821154458202477256070484", "113427455640312821154458202477256070485"); - List tokens = Lists.transform(tokenStrings, new Function() { - @Nullable - @Override - public BigInteger apply(String s) { - return new BigInteger(s); - } - }); + "0", + "1", + "56713727820156410577229101238628035242", + "56713727820156410577229101238628035242", + "113427455640312821154458202477256070484", + "113427455640312821154458202477256070485"); + + List tokens = Lists.transform(tokenStrings, (String string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator("foo.bar.RandomPartitioner"); generator.generateSegments(10, tokens, Boolean.FALSE); @@ -106,42 +103,45 @@ public BigInteger apply(String s) { @Test public void testRotatedRing() throws Exception { List tokenStrings = Lists.newArrayList( - "56713727820156410577229101238628035243", "113427455640312821154458202477256070484", - "113427455640312821154458202477256070485", "5", - "6", "56713727820156410577229101238628035242"); - List tokens = Lists.transform(tokenStrings, new Function() { - @Nullable - @Override - public BigInteger apply(String s) { - return new BigInteger(s); - } - }); + "56713727820156410577229101238628035243", + "113427455640312821154458202477256070484", + "113427455640312821154458202477256070485", + "5", + "6", + "56713727820156410577229101238628035242"); + + List tokens = Lists.transform(tokenStrings, (String string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator("foo.bar.RandomPartitioner"); List segments = generator.generateSegments(10, tokens, Boolean.FALSE); assertEquals(15, segments.size()); + assertEquals( "(113427455640312821154458202477256070484,113427455640312821154458202477256070485]", segments.get(4).toString()); - assertEquals("(5,6]", - segments.get(9).toString()); - assertEquals("(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", - segments.get(14).toString()); + + assertEquals( + + "(5,6]", + segments.get(9).toString()); + + assertEquals( + "(56713727820156410577229101238628035242,56713727820156410577229101238628035243]", + segments.get(14).toString()); } @Test(expected = ReaperException.class) public void testDisorderedRing() throws Exception { + List tokenStrings = Lists.newArrayList( - "0", "113427455640312821154458202477256070485", "1", - "56713727820156410577229101238628035242", "56713727820156410577229101238628035243", + "0", + "113427455640312821154458202477256070485", + "1", + "56713727820156410577229101238628035242", + "56713727820156410577229101238628035243", "113427455640312821154458202477256070484"); - List tokens = Lists.transform(tokenStrings, new Function() { - @Nullable - @Override - public BigInteger apply(String s) { - return new BigInteger(s); - } - }); + + List tokens = Lists.transform(tokenStrings, (String string) -> new BigInteger(string)); SegmentGenerator generator = new SegmentGenerator("foo.bar.RandomPartitioner"); generator.generateSegments(10, tokens, Boolean.FALSE); @@ -153,10 +153,10 @@ public BigInteger apply(String s) { public void testMax() throws Exception { BigInteger one = BigInteger.ONE; BigInteger ten = BigInteger.TEN; - BigInteger minusTen = BigInteger.TEN.negate(); assertEquals(ten, SegmentGenerator.max(one, ten)); assertEquals(ten, SegmentGenerator.max(ten, one)); assertEquals(one, SegmentGenerator.max(one, one)); + BigInteger minusTen = BigInteger.TEN.negate(); assertEquals(one, SegmentGenerator.max(one, minusTen)); } @@ -164,10 +164,10 @@ public void testMax() throws Exception { public void testMin() throws Exception { BigInteger one = BigInteger.ONE; BigInteger ten = BigInteger.TEN; - BigInteger minusTen = BigInteger.TEN.negate(); assertEquals(one, SegmentGenerator.min(one, ten)); assertEquals(one, SegmentGenerator.min(ten, one)); assertEquals(one, SegmentGenerator.min(one, one)); + BigInteger minusTen = BigInteger.TEN.negate(); assertEquals(minusTen, SegmentGenerator.min(one, minusTen)); } @@ -175,10 +175,10 @@ public void testMin() throws Exception { public void testLowerThan() throws Exception { BigInteger one = BigInteger.ONE; BigInteger ten = BigInteger.TEN; - BigInteger minusTen = BigInteger.TEN.negate(); assertTrue(SegmentGenerator.lowerThan(one, ten)); assertFalse(SegmentGenerator.lowerThan(ten, one)); assertFalse(SegmentGenerator.lowerThan(ten, ten)); + BigInteger minusTen = BigInteger.TEN.negate(); assertTrue(SegmentGenerator.lowerThan(minusTen, one)); assertFalse(SegmentGenerator.lowerThan(one, minusTen)); } @@ -187,10 +187,10 @@ public void testLowerThan() throws Exception { public void testGreaterThan() throws Exception { BigInteger one = BigInteger.ONE; BigInteger ten = BigInteger.TEN; - BigInteger minusTen = BigInteger.TEN.negate(); assertTrue(SegmentGenerator.greaterThan(ten, one)); assertFalse(SegmentGenerator.greaterThan(one, ten)); assertFalse(SegmentGenerator.greaterThan(one, one)); + BigInteger minusTen = BigInteger.TEN.negate(); assertFalse(SegmentGenerator.greaterThan(minusTen, one)); assertTrue(SegmentGenerator.greaterThan(one, minusTen)); } diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentRunnerTest.java b/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentRunnerTest.java index 8999eada6..5b79fc13d 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentRunnerTest.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/SegmentRunnerTest.java @@ -11,17 +11,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.spotify.reaper.unit.service; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyCollectionOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import com.spotify.reaper.AppContext; +import com.spotify.reaper.ReaperApplicationConfiguration; +import com.spotify.reaper.ReaperApplicationConfiguration.DatacenterAvailability; +import com.spotify.reaper.ReaperException; +import com.spotify.reaper.cassandra.JmxConnectionFactory; +import com.spotify.reaper.cassandra.JmxProxy; +import com.spotify.reaper.cassandra.RepairStatusHandler; +import com.spotify.reaper.core.RepairRun; +import com.spotify.reaper.core.RepairSegment; +import com.spotify.reaper.core.RepairUnit; +import com.spotify.reaper.service.RepairRunner; +import com.spotify.reaper.service.RingRange; +import com.spotify.reaper.service.SegmentRunner; +import com.spotify.reaper.storage.IStorage; +import com.spotify.reaper.storage.MemoryStorage; import java.math.BigInteger; import java.util.Collections; @@ -31,40 +38,33 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.service.ActiveRepairService; import org.apache.commons.lang3.mutable.MutableObject; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; -import org.mockito.Matchers; import org.mockito.Mockito; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.spotify.reaper.AppContext; -import com.spotify.reaper.ReaperApplicationConfiguration; -import com.spotify.reaper.ReaperApplicationConfiguration.DatacenterAvailability; -import com.spotify.reaper.ReaperException; -import com.spotify.reaper.cassandra.JmxConnectionFactory; -import com.spotify.reaper.cassandra.JmxProxy; -import com.spotify.reaper.cassandra.RepairStatusHandler; -import com.spotify.reaper.core.RepairRun; -import com.spotify.reaper.core.RepairSegment; -import com.spotify.reaper.core.RepairUnit; -import com.spotify.reaper.service.RepairRunner; -import com.spotify.reaper.service.RingRange; -import com.spotify.reaper.service.SegmentRunner; -import com.spotify.reaper.storage.IStorage; -import com.spotify.reaper.storage.MemoryStorage; -public class SegmentRunnerTest { +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class SegmentRunnerTest { // TODO: Clean up tests. There's a lot of code duplication across these tests. @Before public void setUp() throws Exception { - SegmentRunner.segmentRunners.clear(); + SegmentRunner.SEGMENT_RUNNERS.clear(); } @Test @@ -89,7 +89,9 @@ public void timeoutTest() throws InterruptedException, ReaperException, Executio context.jmxConnectionFactory = new JmxConnectionFactory() { @Override - public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + public JmxProxy connect(final Optional handler, String host, int connectionTimeout) + throws ReaperException { + JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn("reaper"); when(jmx.isConnectionAlive()).thenReturn(true); @@ -97,30 +99,54 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter()).thenReturn("dc1"); when(jmx.getDataCenter(anyString())).thenReturn("dc1"); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), - Matchers.any(), Sets.newHashSet(anyString()), anyBoolean(), - anyCollectionOf(String.class))) + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) .then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, context.storage.getRepairSegment(runId, segmentId).get().getState()); - future.setValue(executor.submit(new Thread() { - @Override - public void run() { - handler.get().handle(1, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), - "Repair command 1 has started"); - assertEquals(RepairSegment.State.RUNNING, - context.storage.getRepairSegment(runId, segmentId).get().getState()); - } - })); - return 1; - }); + + assertEquals( + RepairSegment.State.NOT_STARTED, + context.storage.getRepairSegment(runId, segmentId).get().getState()); + + future.setValue(executor.submit(new Thread() { + @Override + public void run() { + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.STARTED), + Optional.absent(), + "Repair command 1 has started"); + + assertEquals( + RepairSegment.State.RUNNING, + context.storage.getRepairSegment(runId, segmentId).get().getState()); + } + })); + return 1; + }); return jmx; } }; RepairRunner rr = mock(RepairRunner.class); RepairUnit ru = mock(RepairUnit.class); - SegmentRunner sr = new SegmentRunner(context, segmentId, Collections.singleton(""), 100, 0.5, - RepairParallelism.PARALLEL, "reaper", ru, rr); + + SegmentRunner sr = new SegmentRunner( + context, + segmentId, + Collections.singleton(""), + 100, + 0.5, + RepairParallelism.PARALLEL, + "reaper", + ru, + rr); + sr.run(); future.getValue().get(); @@ -150,9 +176,12 @@ public void successTest() throws InterruptedException, ReaperException, Executio context.config = Mockito.mock(ReaperApplicationConfiguration.class); when(context.config.getJmxConnectionTimeoutInSeconds()).thenReturn(30); when(context.config.getDatacenterAvailability()).thenReturn(DatacenterAvailability.ALL); + context.jmxConnectionFactory = new JmxConnectionFactory() { @Override - public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + public JmxProxy connect(final Optional handler, String host, int connectionTimeout) + throws ReaperException { + JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn("reaper"); when(jmx.isConnectionAlive()).thenReturn(true); @@ -160,35 +189,72 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter()).thenReturn("dc1"); when(jmx.getDataCenter(anyString())).thenReturn("dc1"); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), - Matchers.any(), Sets.newHashSet(anyString()), anyBoolean(), - anyCollectionOf(String.class))) - .then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(runId, segmentId).get().getState()); - future.setValue(executor.submit(() -> { - handler.get().handle(1, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), - "Repair command 1 has started"); - assertEquals(RepairSegment.State.RUNNING, storage.getRepairSegment(runId, segmentId).get().getState()); - // report about an unrelated repair. Shouldn't affect anything. - handler.get().handle(2, Optional.of(ActiveRepairService.Status.SESSION_FAILED), Optional.absent(), - "Repair command 2 has failed"); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), Optional.absent(), - "Repair session succeeded in command 1"); - assertEquals(RepairSegment.State.DONE, storage.getRepairSegment(runId, segmentId).get().getState()); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.FINISHED), Optional.absent(), - "Repair command 1 has finished"); - assertEquals(RepairSegment.State.DONE, storage.getRepairSegment(runId, segmentId).get().getState()); - })); - return 1; - }); + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) + .then(invocation -> { + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(runId, segmentId).get().getState()); + + future.setValue(executor.submit(() -> { + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.STARTED), + Optional.absent(), + "Repair command 1 has started"); + + assertEquals(RepairSegment.State.RUNNING, storage.getRepairSegment(runId, segmentId).get().getState()); + // report about an unrelated repair. Shouldn't affect anything. + handler.get().handle( + 2, + Optional.of(ActiveRepairService.Status.SESSION_FAILED), + Optional.absent(), + "Repair command 2 has failed"); + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.SESSION_SUCCESS), + Optional.absent(), + "Repair session succeeded in command 1"); + + assertEquals(RepairSegment.State.DONE, storage.getRepairSegment(runId, segmentId).get().getState()); + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.FINISHED), + Optional.absent(), + "Repair command 1 has finished"); + + assertEquals(RepairSegment.State.DONE, storage.getRepairSegment(runId, segmentId).get().getState()); + })); + return 1; + }); return jmx; } }; RepairRunner rr = mock(RepairRunner.class); RepairUnit ru = mock(RepairUnit.class); - SegmentRunner sr = new SegmentRunner(context, segmentId, Collections.singleton(""), 1000, 0.5, - RepairParallelism.PARALLEL, "reaper", ru, rr); + + SegmentRunner sr = new SegmentRunner( + context, + segmentId, + Collections.singleton(""), + 1000, + 0.5, + RepairParallelism.PARALLEL, + "reaper", + ru, + rr); + sr.run(); future.getValue().get(); @@ -218,9 +284,12 @@ public void failureTest() throws InterruptedException, ReaperException, Executio context.config = Mockito.mock(ReaperApplicationConfiguration.class); when(context.config.getJmxConnectionTimeoutInSeconds()).thenReturn(30); when(context.config.getDatacenterAvailability()).thenReturn(DatacenterAvailability.ALL); + context.jmxConnectionFactory = new JmxConnectionFactory() { @Override - public JmxProxy connect(final Optional handler, String host, int connectionTimeout) throws ReaperException { + public JmxProxy connect(final Optional handler, String host, int connectionTimeout) + throws ReaperException { + JmxProxy jmx = mock(JmxProxy.class); when(jmx.getClusterName()).thenReturn("reaper"); when(jmx.isConnectionAlive()).thenReturn(true); @@ -228,33 +297,71 @@ public JmxProxy connect(final Optional handler, String host when(jmx.getDataCenter()).thenReturn("dc1"); when(jmx.getDataCenter(anyString())).thenReturn("dc1"); - when(jmx.triggerRepair(any(BigInteger.class), any(BigInteger.class), anyString(), - Matchers.any(), Sets.newHashSet(anyString()), anyBoolean(), - anyCollectionOf(String.class))) + when(jmx.triggerRepair( + any(BigInteger.class), + any(BigInteger.class), + any(), + any(RepairParallelism.class), + any(), + anyBoolean(), + any())) .then((invocation) -> { - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(runId, segmentId).get().getState()); - future.setValue(executor.submit(() -> { - handler.get().handle(1, Optional.of(ActiveRepairService.Status.STARTED), Optional.absent(), - "Repair command 1 has started"); - assertEquals(RepairSegment.State.RUNNING, storage.getRepairSegment(runId, segmentId).get().getState()); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.SESSION_FAILED), Optional.absent(), - "Repair command 1 has failed"); - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(runId, segmentId).get().getState()); - handler.get().handle(1, Optional.of(ActiveRepairService.Status.FINISHED), Optional.absent(), - "Repair command 1 has finished"); - assertEquals(RepairSegment.State.NOT_STARTED, storage.getRepairSegment(runId, segmentId).get().getState()); - })); - - return 1; - }); + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(runId, segmentId).get().getState()); + + future.setValue(executor.submit(() -> { + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.STARTED), + Optional.absent(), + "Repair command 1 has started"); + + assertEquals(RepairSegment.State.RUNNING, storage.getRepairSegment(runId, segmentId).get().getState()); + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.SESSION_FAILED), + Optional.absent(), + "Repair command 1 has failed"); + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(runId, segmentId).get().getState()); + + handler.get().handle( + 1, + Optional.of(ActiveRepairService.Status.FINISHED), + Optional.absent(), + "Repair command 1 has finished"); + + assertEquals( + RepairSegment.State.NOT_STARTED, + storage.getRepairSegment(runId, segmentId).get().getState()); + })); + + return 1; + }); return jmx; } }; RepairRunner rr = mock(RepairRunner.class); RepairUnit ru = mock(RepairUnit.class); - SegmentRunner sr = new SegmentRunner(context, segmentId, Collections.singleton(""), 1000, 0.5, - RepairParallelism.PARALLEL, "reaper", ru, rr); + + SegmentRunner sr = new SegmentRunner( + context, + segmentId, + Collections.singleton(""), + 1000, + 0.5, + RepairParallelism.PARALLEL, + "reaper", + ru, + rr); + sr.run(); future.getValue().get(); diff --git a/src/server/src/test/java/com/spotify/reaper/unit/service/TestRepairConfiguration.java b/src/server/src/test/java/com/spotify/reaper/unit/service/TestRepairConfiguration.java index 817f799a9..012a2503b 100644 --- a/src/server/src/test/java/com/spotify/reaper/unit/service/TestRepairConfiguration.java +++ b/src/server/src/test/java/com/spotify/reaper/unit/service/TestRepairConfiguration.java @@ -1,10 +1,28 @@ +/* + * 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.spotify.reaper.unit.service; import com.spotify.reaper.ReaperApplicationConfiguration; import com.spotify.reaper.ReaperApplicationConfigurationBuilder; +import com.spotify.reaper.ReaperApplicationConfigurationBuilder.AutoSchedulingConfigurationBuilder; + import org.apache.cassandra.repair.RepairParallelism; -public class TestRepairConfiguration { +public final class TestRepairConfiguration { + + private TestRepairConfiguration() {} public static ReaperApplicationConfiguration defaultConfig() { return defaultConfigBuilder().build(); @@ -23,7 +41,7 @@ public static ReaperApplicationConfigurationBuilder defaultConfigBuilder() { .withAutoScheduling(defaultAutoSchedulingConfigBuilder().build()); } - public static ReaperApplicationConfigurationBuilder.AutoSchedulingConfigurationBuilder defaultAutoSchedulingConfigBuilder() { - return new ReaperApplicationConfigurationBuilder.AutoSchedulingConfigurationBuilder().thatIsDisabled(); + public static AutoSchedulingConfigurationBuilder defaultAutoSchedulingConfigBuilder() { + return new AutoSchedulingConfigurationBuilder().thatIsDisabled(); } } diff --git a/src/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/src/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file