diff --git a/README.md b/README.md
index c99efbe16..2c28f8c82 100644
--- a/README.md
+++ b/README.md
@@ -159,6 +159,12 @@ Source code for all the REST resources can be found from package com.spotify.rea
* Adds a new cluster to the service, and returns the newly added cluster object,
if the operation was successful.
+* DELETE /cluster/{cluster_name}
+ * Expected query parameters: *None*
+ * Delete a cluster object identified by the given "cluster_name" path parameter.
+ Cluster will get deleted only if there are no schedules or repair runs for the cluster,
+ or the request will fail. Delete repair runs and schedules first before calling this.
+
## Repair Run Resource
* GET /repair_run
@@ -193,6 +199,13 @@ Source code for all the REST resources can be found from package com.spotify.rea
Possible values for given state are: "PAUSED" or "RUNNING".
* Starts, pauses, or resumes a repair run identified by the "id" path parameter.
+* DELETE /repair_run/{id}
+ * Expected query parameters:
+ * *owner*: Owner name for the run. If the given owner does not match the stored owner,
+ the delete request will fail.
+ * Delete a repair run object identified by the given "id" path parameter.
+ Repair run and all the related repair segments will be deleted from the database.
+
## Repair Schedule Resource
* GET /repair_schedule
@@ -218,3 +231,11 @@ Source code for all the REST resources can be found from package com.spotify.rea
* *scheduleTriggerTime*: Defines the time for first scheduled trigger for the run.
If you don't give this value, it will be next mid-night (UTC).
Give date values in ISO format, e.g. "2015-02-11T01:00:00". (Optional)
+
+* DELETE /repair_schedule/{id}
+ * Expected query parameters:
+ * *owner*: Owner name for the schedule. If the given owner does not match the stored owner,
+ the delete request will fail.
+ * Delete a repair schedule object identified by the given "id" path parameter.
+ Repair schedule will get deleted only if there are no associated repair runs for the schedule.
+ Delete all the related repair runs before calling this endpoint.
diff --git a/pom.xml b/pom.xml
index 76c5ea5aa..b4ae9b949 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,10 @@
org.slf4j
slf4j-log4j12
+
+ org.codehaus.jackson
+ *
+
diff --git a/src/main/java/com/spotify/reaper/core/RepairRun.java b/src/main/java/com/spotify/reaper/core/RepairRun.java
index e4f19d8fe..5c3d6c3e5 100644
--- a/src/main/java/com/spotify/reaper/core/RepairRun.java
+++ b/src/main/java/com/spotify/reaper/core/RepairRun.java
@@ -120,7 +120,8 @@ public enum RunState {
RUNNING,
ERROR,
DONE,
- PAUSED
+ PAUSED,
+ DELETED
}
public static class Builder {
diff --git a/src/main/java/com/spotify/reaper/core/RepairSchedule.java b/src/main/java/com/spotify/reaper/core/RepairSchedule.java
index de8abb730..4f4293934 100644
--- a/src/main/java/com/spotify/reaper/core/RepairSchedule.java
+++ b/src/main/java/com/spotify/reaper/core/RepairSchedule.java
@@ -116,11 +116,11 @@ public Builder with() {
}
public enum State {
- RUNNING,
- PAUSED
+ ACTIVE,
+ PAUSED,
+ DELETED
}
-
public static class Builder {
public final long repairUnitId;
diff --git a/src/main/java/com/spotify/reaper/resources/ClusterResource.java b/src/main/java/com/spotify/reaper/resources/ClusterResource.java
index f4a3e331a..8301b5b5e 100644
--- a/src/main/java/com/spotify/reaper/resources/ClusterResource.java
+++ b/src/main/java/com/spotify/reaper/resources/ClusterResource.java
@@ -34,6 +34,7 @@
import java.util.Collections;
import java.util.List;
+import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -195,4 +196,40 @@ private Response viewKeyspace(Cluster cluster, String keyspaceName) {
return Response.ok().entity(view).build();
}
+ /**
+ * 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.
+ *
+ * @param clusterName The name of the Cluster instance you are about to delete.
+ * @return The deleted RepairRun instance, with state overwritten to string "DELETED".
+ */
+ @DELETE
+ @Path("/{cluster_name}")
+ public Response deleteCluster(@PathParam("cluster_name") String clusterName) {
+ 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();
+ }
+ 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();
+ }
+ 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();
+ }
+ Optional deletedCluster = context.storage.deleteCluster(clusterName);
+ if (deletedCluster.isPresent()) {
+ return Response.ok().entity(new ClusterStatus(deletedCluster.get())).build();
+ }
+ return Response.serverError().entity("delete failed for schedule with name \""
+ + clusterName + "\"").build();
+ }
+
}
diff --git a/src/main/java/com/spotify/reaper/resources/CommonTools.java b/src/main/java/com/spotify/reaper/resources/CommonTools.java
index 5a515621c..a1ac37d12 100644
--- a/src/main/java/com/spotify/reaper/resources/CommonTools.java
+++ b/src/main/java/com/spotify/reaper/resources/CommonTools.java
@@ -85,6 +85,8 @@ private static List generateSegments(AppContext context, Cluster targ
int segmentCount)
throws ReaperException {
List segments = null;
+ assert targetCluster.getPartitioner() != null :
+ "no partitioner for cluster: " + targetCluster.getName();
SegmentGenerator sg = new SegmentGenerator(targetCluster.getPartitioner());
Set seedHosts = targetCluster.getSeedHosts();
if (seedHosts.isEmpty()) {
@@ -179,8 +181,9 @@ public static RepairSchedule storeNewRepairSchedule(
Double intensity)
throws ReaperException {
RepairSchedule.Builder scheduleBuilder =
- new RepairSchedule.Builder(repairUnit.getId(), RepairSchedule.State.RUNNING, daysBetween,
- nextActivation, ImmutableList.of(), segments, repairParallelism, intensity,
+ new RepairSchedule.Builder(repairUnit.getId(), RepairSchedule.State.ACTIVE, daysBetween,
+ nextActivation, ImmutableList.of(), segments,
+ repairParallelism, intensity,
DateTime.now());
scheduleBuilder.owner(owner);
RepairSchedule newRepairSchedule = context.storage.addRepairSchedule(scheduleBuilder);
@@ -243,6 +246,9 @@ public static RepairUnit getNewOrExistingRepairUnit(AppContext context, Cluster
}
public static String dateTimeToISO8601(DateTime dateTime) {
+ if (null == dateTime) {
+ return null;
+ }
return ISODateTimeFormat.dateTimeNoMillis().print(dateTime);
}
diff --git a/src/main/java/com/spotify/reaper/resources/RepairRunResource.java b/src/main/java/com/spotify/reaper/resources/RepairRunResource.java
index 2ce2bffc2..261f20024 100644
--- a/src/main/java/com/spotify/reaper/resources/RepairRunResource.java
+++ b/src/main/java/com/spotify/reaper/resources/RepairRunResource.java
@@ -41,6 +41,7 @@
import java.util.Set;
import javax.annotation.Nullable;
+import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
@@ -243,7 +244,13 @@ public Response modifyRunState(
return Response.status(Response.Status.NOT_FOUND).entity(errMsg).build();
}
- RepairRun.RunState newState = RepairRun.RunState.valueOf(state.get());
+ RepairRun.RunState newState;
+ try {
+ newState = RepairRun.RunState.valueOf(state.get().toUpperCase());
+ } catch (IllegalArgumentException ex) {
+ return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
+ .entity("invalid \"state\" argument: " + state.get()).build();
+ }
RepairRun.RunState oldState = repairRun.get().getRunState();
if (oldState == newState) {
@@ -401,7 +408,7 @@ public Set splitStateParam(Optional state) {
Iterable chunks = CommonTools.COMMA_SEPARATED_LIST_SPLITTER.split(state.get());
for (String chunk : chunks) {
try {
- RepairRun.RunState.valueOf(chunk);
+ RepairRun.RunState.valueOf(chunk.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Listing repair runs called with erroneous states: {}", state.get());
return null;
@@ -413,4 +420,56 @@ 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.
+ *
+ * @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") Long 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();
+ }
+ Optional runToDelete = context.storage.getRepairRun(runId);
+ if (!runToDelete.isPresent()) {
+ return Response.status(Response.Status.NOT_FOUND).entity(
+ "Repair run with id \"" + runId + "\" not found").build();
+ }
+ 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();
+ }
+ 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();
+ }
+ if (context.storage.getSegmentAmountForRepairRun(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();
+ }
+ // Need to get the RepairUnit before it's possibly deleted.
+ Optional unitPossiblyDeleted =
+ context.storage.getRepairUnit(runToDelete.get().getRepairUnitId());
+ Optional deletedRun = context.storage.deleteRepairRun(runId);
+ if (deletedRun.isPresent()) {
+ RepairRunStatus repairRunStatus = new RepairRunStatus(deletedRun.get(),
+ unitPossiblyDeleted.get());
+ return Response.ok().entity(repairRunStatus).build();
+ }
+ return Response.serverError().entity("delete failed for repair run with id \""
+ + runId + "\"").build();
+ }
+
}
diff --git a/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java b/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java
index 2719af13d..c56dd59da 100644
--- a/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java
+++ b/src/main/java/com/spotify/reaper/resources/RepairScheduleResource.java
@@ -14,8 +14,8 @@
package com.spotify.reaper.resources;
import com.google.common.base.Optional;
-
import com.google.common.collect.Lists;
+
import com.spotify.reaper.AppContext;
import com.spotify.reaper.ReaperException;
import com.spotify.reaper.core.Cluster;
@@ -38,6 +38,7 @@
import java.util.List;
import java.util.Set;
+import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
@@ -170,11 +171,11 @@ public Response addRepairSchedule(
}
/**
- * Modifies a state of the repair schedule. Currently supports PAUSED -> RUNNING and
- * RUNNING -> PAUSED.
+ * Modifies a state of the repair schedule. 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 501
- * (NOT_IMPLEMENTED) if transition is not supported.
+ * @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}")
@@ -184,7 +185,7 @@ public Response modifyState(
@QueryParam("state") Optional state) {
LOG.info("modify repair schedule state called with: id = {}, state = {}",
- repairScheduleId, state);
+ repairScheduleId, state);
if (!state.isPresent()) {
return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
@@ -194,7 +195,7 @@ public Response modifyState(
Optional repairSchedule = context.storage.getRepairSchedule(repairScheduleId);
if (!repairSchedule.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).entity("repair schedule with id "
- + repairScheduleId + " not found")
+ + repairScheduleId + " not found")
.build();
}
@@ -207,7 +208,13 @@ public Response modifyState(
return Response.status(Response.Status.NOT_FOUND).entity(errMsg).build();
}
- RepairSchedule.State newState = RepairSchedule.State.valueOf(state.get());
+ RepairSchedule.State newState;
+ try {
+ newState = RepairSchedule.State.valueOf(state.get().toUpperCase());
+ } catch (IllegalArgumentException ex) {
+ return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
+ .entity("invalid \"state\" argument: " + state.get()).build();
+ }
RepairSchedule.State oldState = repairSchedule.get().getState();
if (oldState == newState) {
@@ -220,18 +227,18 @@ public Response modifyState(
return resumeSchedule(repairSchedule.get(), repairUnit.get());
} else {
String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(),
- newState.toString());
+ newState.toString());
LOG.error(errMsg);
return Response.status(Response.Status.BAD_REQUEST).entity(errMsg).build();
}
}
private static boolean isPausing(RepairSchedule.State oldState, RepairSchedule.State newState) {
- return oldState == RepairSchedule.State.RUNNING && newState == RepairSchedule.State.PAUSED;
+ return oldState == RepairSchedule.State.ACTIVE && newState == RepairSchedule.State.PAUSED;
}
private static boolean isResuming(RepairSchedule.State oldState, RepairSchedule.State newState) {
- return oldState == RepairSchedule.State.PAUSED && newState == RepairSchedule.State.RUNNING;
+ return oldState == RepairSchedule.State.PAUSED && newState == RepairSchedule.State.ACTIVE;
}
private Response pauseSchedule(RepairSchedule repairSchedule, RepairUnit repairUnit) {
@@ -328,4 +335,47 @@ public Response listSchedules() {
return Response.status(Response.Status.OK).entity(scheduleStatuses).build();
}
+ /**
+ * 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.
+ * @return The deleted RepairSchedule instance, with state overwritten to string "DELETED".
+ */
+ @DELETE
+ @Path("/{id}")
+ public Response deleteRepairSchedule(@PathParam("id") Long repairScheduleId,
+ @QueryParam("owner") Optional 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();
+ }
+ Optional scheduleToDelete = context.storage.getRepairSchedule(repairScheduleId);
+ if (!scheduleToDelete.isPresent()) {
+ return Response.status(Response.Status.NOT_FOUND).entity(
+ "Repair schedule with id \"" + repairScheduleId + "\" not found").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();
+ }
+ Optional deletedSchedule =
+ context.storage.deleteRepairSchedule(repairScheduleId);
+ if (deletedSchedule.isPresent()) {
+ return Response.ok().entity(getRepairScheduleStatus(deletedSchedule.get())).build();
+ }
+ return Response.serverError().entity("delete failed for schedule with id \""
+ + repairScheduleId + "\"").build();
+ }
+
}
diff --git a/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java b/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java
index 9565abc57..a880283bc 100644
--- a/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java
+++ b/src/main/java/com/spotify/reaper/resources/view/RepairRunStatus.java
@@ -20,6 +20,7 @@
import com.spotify.reaper.resources.CommonTools;
import org.joda.time.DateTime;
+import org.joda.time.format.ISODateTimeFormat;
import java.util.Collection;
@@ -29,52 +30,58 @@
public class RepairRunStatus {
@JsonProperty
- private final String cause;
+ private String cause;
@JsonProperty
- private final String owner;
+ private String owner;
@JsonProperty
- private final long id;
+ private long id;
@JsonProperty("cluster_name")
- private final String clusterName;
+ private String clusterName;
@JsonProperty("column_families")
- private final Collection columnFamilies;
+ private Collection columnFamilies;
@JsonProperty("keyspace_name")
- private final String keyspaceName;
+ private String keyspaceName;
@JsonProperty("run_state")
- private final String runState;
+ private String runState;
@JsonIgnore
- private final DateTime creationTime;
+ private DateTime creationTime;
@JsonIgnore
- private final DateTime startTime;
+ private DateTime startTime;
@JsonIgnore
- private final DateTime endTime;
+ private DateTime endTime;
@JsonIgnore
- private final DateTime pauseTime;
+ private DateTime pauseTime;
@JsonProperty
- private final double intensity;
+ private double intensity;
@JsonProperty("segment_count")
- private final int segmentCount;
+ private int segmentCount;
@JsonProperty("repair_parallelism")
- private final String repairParallelism;
+ private String repairParallelism;
@JsonProperty("segments_repaired")
private int segmentsRepaired = 0;
@JsonProperty("last_event")
- private final String lastEvent;
+ private String lastEvent;
+
+ /**
+ * Default public constructor Required for Jackson JSON parsing.
+ */
+ public RepairRunStatus() {
+ }
public RepairRunStatus(RepairRun repairRun, RepairUnit repairUnit) {
this.id = repairRun.getId();
@@ -102,6 +109,13 @@ public String getCreationTimeISO8601() {
return CommonTools.dateTimeToISO8601(creationTime);
}
+ @JsonProperty("creation_time")
+ public void setCreationTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ creationTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
+ }
+
@JsonProperty("start_time")
public String getStartTimeISO8601() {
if (startTime == null) {
@@ -110,6 +124,13 @@ public String getStartTimeISO8601() {
return CommonTools.dateTimeToISO8601(startTime);
}
+ @JsonProperty("start_time")
+ public void setStartTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ startTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
+ }
+
@JsonProperty("end_time")
public String getEndTimeISO8601() {
if (endTime == null) {
@@ -118,6 +139,13 @@ public String getEndTimeISO8601() {
return CommonTools.dateTimeToISO8601(endTime);
}
+ @JsonProperty("end_time")
+ public void setEndTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ endTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
+ }
+
@JsonProperty("pause_time")
public String getPauseTimeISO8601() {
if (pauseTime == null) {
@@ -126,15 +154,138 @@ public String getPauseTimeISO8601() {
return CommonTools.dateTimeToISO8601(pauseTime);
}
- public void setSegmentsRepaired(int segmentsRepaired) {
- this.segmentsRepaired = segmentsRepaired;
+ @JsonProperty("pause_time")
+ public void setPauseTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ pauseTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
+ }
+
+ public String getCause() {
+ return cause;
+ }
+
+ public void setCause(String cause) {
+ this.cause = cause;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
}
public long getId() {
- return this.id;
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public Collection getColumnFamilies() {
+ return columnFamilies;
+ }
+
+ public void setColumnFamilies(Collection columnFamilies) {
+ this.columnFamilies = columnFamilies;
+ }
+
+ public String getKeyspaceName() {
+ return keyspaceName;
+ }
+
+ public void setKeyspaceName(String keyspaceName) {
+ this.keyspaceName = keyspaceName;
}
public String getRunState() {
- return this.runState;
+ return runState;
+ }
+
+ public void setRunState(String runState) {
+ this.runState = runState;
+ }
+
+ public DateTime getCreationTime() {
+ return creationTime;
+ }
+
+ public void setCreationTime(DateTime creationTime) {
+ this.creationTime = creationTime;
+ }
+
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(DateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public DateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(DateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public DateTime getPauseTime() {
+ return pauseTime;
+ }
+
+ public void setPauseTime(DateTime pauseTime) {
+ this.pauseTime = pauseTime;
+ }
+
+ public double getIntensity() {
+ return intensity;
+ }
+
+ public void setIntensity(double intensity) {
+ this.intensity = intensity;
+ }
+
+ public int getSegmentCount() {
+ return segmentCount;
+ }
+
+ public void setSegmentCount(int segmentCount) {
+ this.segmentCount = segmentCount;
+ }
+
+ public String getRepairParallelism() {
+ return repairParallelism;
+ }
+
+ public void setRepairParallelism(String repairParallelism) {
+ this.repairParallelism = repairParallelism;
+ }
+
+ public int getSegmentsRepaired() {
+ return segmentsRepaired;
+ }
+
+ public void setSegmentsRepaired(int segmentsRepaired) {
+ this.segmentsRepaired = segmentsRepaired;
+ }
+
+ public String getLastEvent() {
+ return lastEvent;
+ }
+
+ public void setLastEvent(String lastEvent) {
+ this.lastEvent = lastEvent;
}
}
diff --git a/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java b/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java
index 91e11d5cb..3705ec020 100644
--- a/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java
+++ b/src/main/java/com/spotify/reaper/resources/view/RepairScheduleStatus.java
@@ -13,8 +13,6 @@
*/
package com.spotify.reaper.resources.view;
-import com.google.common.annotations.VisibleForTesting;
-
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.spotify.reaper.core.RepairSchedule;
@@ -22,49 +20,56 @@
import com.spotify.reaper.resources.CommonTools;
import org.joda.time.DateTime;
+import org.joda.time.format.ISODateTimeFormat;
import java.util.Collection;
public class RepairScheduleStatus {
- @JsonProperty
- private final long id;
+ @JsonProperty()
+ private long id;
- @JsonProperty
- private final String owner;
+ @JsonProperty()
+ private String owner;
@JsonProperty("cluster_name")
- private final String clusterName;
+ private String clusterName;
@JsonProperty("column_families")
- private final Collection columnFamilies;
+ private Collection columnFamilies;
@JsonProperty("keyspace_name")
- private final String keyspaceName;
+ private String keyspaceName;
- @JsonProperty("state")
- private final String state;
+ @JsonProperty()
+ private String state;
@JsonIgnore
- private final DateTime creationTime;
+ private DateTime creationTime;
@JsonIgnore
- private final DateTime nextActivation;
+ private DateTime nextActivation;
@JsonIgnore
- private final DateTime pauseTime;
+ private DateTime pauseTime;
- @JsonProperty
- private final double intensity;
+ @JsonProperty()
+ private double intensity;
@JsonProperty("segment_count")
- private final int segmentCount;
+ private int segmentCount;
@JsonProperty("repair_parallelism")
- private final String repairParallelism;
+ private String repairParallelism;
+
+ @JsonProperty("scheduled_days_between")
+ private int daysBetween;
- @JsonProperty("schedule_days_between")
- private final int daysBetween;
+ /**
+ * Default public constructor Required for Jackson JSON parsing.
+ */
+ public RepairScheduleStatus() {
+ }
public RepairScheduleStatus(RepairSchedule repairSchedule, RepairUnit repairUnit) {
this.id = repairSchedule.getId();
@@ -82,36 +87,144 @@ public RepairScheduleStatus(RepairSchedule repairSchedule, RepairUnit repairUnit
this.daysBetween = repairSchedule.getDaysBetween();
}
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public Collection getColumnFamilies() {
+ return columnFamilies;
+ }
+
+ public void setColumnFamilies(Collection columnFamilies) {
+ this.columnFamilies = columnFamilies;
+ }
+
+ public String getKeyspaceName() {
+ return keyspaceName;
+ }
+
+ public void setKeyspaceName(String keyspaceName) {
+ this.keyspaceName = keyspaceName;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public DateTime getCreationTime() {
+ return creationTime;
+ }
+
+ public void setCreationTime(DateTime creationTime) {
+ this.creationTime = creationTime;
+ }
+
+ public DateTime getNextActivation() {
+ return nextActivation;
+ }
+
+ public void setNextActivation(DateTime nextActivation) {
+ this.nextActivation = nextActivation;
+ }
+
+ public DateTime getPauseTime() {
+ return pauseTime;
+ }
+
+ public void setPauseTime(DateTime pauseTime) {
+ this.pauseTime = pauseTime;
+ }
+
+ public double getIntensity() {
+ return intensity;
+ }
+
+ public void setIntensity(double intensity) {
+ this.intensity = intensity;
+ }
+
+ public int getSegmentCount() {
+ return segmentCount;
+ }
+
+ public void setSegmentCount(int segmentCount) {
+ this.segmentCount = segmentCount;
+ }
+
+ public String getRepairParallelism() {
+ return repairParallelism;
+ }
+
+ public void setRepairParallelism(String repairParallelism) {
+ this.repairParallelism = repairParallelism;
+ }
+
+ public int getDaysBetween() {
+ return daysBetween;
+ }
+
+ public void setDaysBetween(int daysBetween) {
+ this.daysBetween = daysBetween;
+ }
+
@JsonProperty("creation_time")
public String getCreationTimeISO8601() {
- if (creationTime == null) {
- return null;
- }
return CommonTools.dateTimeToISO8601(creationTime);
}
+ @JsonProperty("creation_time")
+ public void setCreationTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ creationTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
+ }
+
@JsonProperty("next_activation")
public String getNextActivationISO8601() {
- if (nextActivation == null) {
- return null;
- }
return CommonTools.dateTimeToISO8601(nextActivation);
}
- @JsonProperty("pause_time")
- public String getPauseTimeISO8601() {
- if (pauseTime == null) {
- return null;
+ @JsonProperty("next_activation")
+ public void setNextActivationISO8601(String dateStr) {
+ if (null != dateStr) {
+ nextActivation = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
}
- return CommonTools.dateTimeToISO8601(pauseTime);
}
- public long getId() {
- return this.id;
+ @JsonProperty("pause_time")
+ public String getPauseTimeISO8601() {
+ return CommonTools.dateTimeToISO8601(pauseTime);
}
- public String getState() {
- return this.state;
+ @JsonProperty("pause_time")
+ public void setPauseTimeISO8601(String dateStr) {
+ if (null != dateStr) {
+ pauseTime = ISODateTimeFormat.dateTimeNoMillis().parseDateTime(dateStr);
+ }
}
}
diff --git a/src/main/java/com/spotify/reaper/service/RepairRunner.java b/src/main/java/com/spotify/reaper/service/RepairRunner.java
index 042248949..c61b0b9f1 100644
--- a/src/main/java/com/spotify/reaper/service/RepairRunner.java
+++ b/src/main/java/com/spotify/reaper/service/RepairRunner.java
@@ -65,9 +65,16 @@ public Long getCurrentlyRunningSegmentId() {
*/
@Override
public void run() {
- RepairRun repairRun = context.storage.getRepairRun(repairRunId).get();
+ Optional repairRun = context.storage.getRepairRun(repairRunId);
try {
- RepairRun.RunState state = repairRun.getRunState();
+ if (!repairRun.isPresent()) {
+ // this might happen if a run is deleted while paused etc.
+ LOG.warn("RepairRun \"" + repairRunId + "\" does not exist. Killing "
+ + "RepairRunner for this run instance.");
+ context.repairManager.removeRunner(this);
+ return;
+ }
+ RepairRun.RunState state = repairRun.get().getRunState();
LOG.debug("run() called for repair run #{} with run state {}", repairRunId, state);
switch (state) {
case NOT_STARTED:
@@ -89,10 +96,12 @@ public void run() {
LOG.error(e.toString());
LOG.error(Arrays.toString(e.getStackTrace()));
e.printStackTrace();
- context.storage.updateRepairRun(repairRun.with()
- .runState(RepairRun.RunState.ERROR)
- .endTime(DateTime.now())
- .build(repairRun.getId()));
+ if (repairRun.isPresent()) {
+ context.storage.updateRepairRun(repairRun.get().with()
+ .runState(RepairRun.RunState.ERROR)
+ .endTime(DateTime.now())
+ .build(repairRunId));
+ }
context.repairManager.removeRunner(this);
}
}
diff --git a/src/main/java/com/spotify/reaper/service/SchedulingManager.java b/src/main/java/com/spotify/reaper/service/SchedulingManager.java
index 7ddcad575..099e0f01a 100644
--- a/src/main/java/com/spotify/reaper/service/SchedulingManager.java
+++ b/src/main/java/com/spotify/reaper/service/SchedulingManager.java
@@ -19,8 +19,6 @@
import java.util.Timer;
import java.util.TimerTask;
-import javax.annotation.Nullable;
-
public class SchedulingManager extends TimerTask {
private static final Logger LOG = LoggerFactory.getLogger(SchedulingManager.class);
@@ -34,7 +32,7 @@ public static void start(AppContext context) {
Timer timer = new Timer("SchedulingManagerTimer");
timer.schedule(schedulingManager, 1000, 1000 * 60); // activate once per minute
} else {
- LOG.warn("there is already one instance of SchedulingManager running, not starting new");
+ LOG.warn("there is already one instance of SchedulingManager running, not starting new one");
}
}
@@ -44,24 +42,23 @@ public static void pauseRepairSchedule(AppContext context, RepairSchedule schedu
.pauseTime(DateTime.now())
.build(schedule.getId());
if (!context.storage.updateRepairSchedule(updatedSchedule)) {
- throw new RuntimeException("failed updating repair schedule " + updatedSchedule.getId());
+ LOG.error("failed updating repair schedule " + updatedSchedule.getId());
}
}
public static void resumeRepairSchedule(AppContext context, RepairSchedule schedule) {
RepairSchedule updatedSchedule = schedule.with()
- .state(RepairSchedule.State.RUNNING)
+ .state(RepairSchedule.State.ACTIVE)
.pauseTime(null)
.build(schedule.getId());
if (!context.storage.updateRepairSchedule(updatedSchedule)) {
- throw new RuntimeException("failed updating repair schedule " + updatedSchedule.getId());
+ LOG.error("failed updating repair schedule " + updatedSchedule.getId());
}
}
private AppContext context;
/* nextActivatedSchedule used for nicer logging only */
- @Nullable
private RepairSchedule nextActivatedSchedule;
private SchedulingManager(AppContext context) {
@@ -80,10 +77,7 @@ public void run() {
boolean anyRunStarted = false;
for (RepairSchedule schedule : schedules) {
lastId = schedule.getId();
- boolean runStarted = manageSchedule(schedule);
- if (runStarted) {
- anyRunStarted = true;
- }
+ anyRunStarted = manageSchedule(schedule) || anyRunStarted;
}
if (!anyRunStarted && nextActivatedSchedule != null) {
LOG.debug("not scheduling new repairs yet, next activation is '{}' for schedule id '{}'",
@@ -113,7 +107,12 @@ private boolean manageSchedule(RepairSchedule schedule) throws ReaperException {
LOG.info("Repair schedule '{}' is paused", schedule.getId());
startNewRun = false;
} else {
- repairUnit = context.storage.getRepairUnit(schedule.getRepairUnitId()).get();
+ Optional fetchedUnit = context.storage.getRepairUnit(schedule.getRepairUnitId());
+ if (!fetchedUnit.isPresent()) {
+ LOG.warn("RepairUnit with id " + schedule.getRepairUnitId() + " not found");
+ return false;
+ }
+ repairUnit = fetchedUnit.get();
Collection repairRuns = context.storage.getRepairRunsForUnit(repairUnit);
for (RepairRun repairRun : repairRuns) {
RepairRun.RunState state = repairRun.getRunState();
diff --git a/src/main/java/com/spotify/reaper/storage/IStorage.java b/src/main/java/com/spotify/reaper/storage/IStorage.java
index 77a4f7042..b0f24405b 100644
--- a/src/main/java/com/spotify/reaper/storage/IStorage.java
+++ b/src/main/java/com/spotify/reaper/storage/IStorage.java
@@ -21,7 +21,6 @@
import com.spotify.reaper.core.RepairSegment;
import com.spotify.reaper.core.RepairUnit;
import com.spotify.reaper.service.RingRange;
-import com.spotify.reaper.service.SchedulingManager;
import java.util.Collection;
import java.util.Set;
@@ -41,6 +40,15 @@ 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.
+ *
+ * @param clusterName The name of the Cluster instance to delete.
+ * @return The deleted Cluster instance if delete succeeds, with state set to DELETED.
+ */
+ Optional deleteCluster(String clusterName);
+
RepairRun addRepairRun(RepairRun.Builder repairRun);
boolean updateRepairRun(RepairRun repairRun);
@@ -53,6 +61,15 @@ 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.
+ *
+ * @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.
+ */
+ Optional deleteRepairRun(long id);
+
RepairUnit addRepairUnit(RepairUnit.Builder newRepairUnit);
Optional getRepairUnit(long id);
@@ -66,7 +83,7 @@ public interface IStorage {
* @return Instance of a RepairUnit matching the parameters, or null if not found.
*/
Optional getRepairUnit(String cluster, String keyspace,
- Set columnFamilyNames);
+ Set columnFamilyNames);
void addRepairSegments(Collection newSegments, long runId);
@@ -94,4 +111,13 @@ Optional getRepairUnit(String cluster, String keyspace,
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.
+ *
+ * @param id The id of the RepairSchedule instance to delete.
+ * @return The deleted RepairSchedule instance, if delete succeeds, with state set to DELETED.
+ */
+ Optional deleteRepairSchedule(long id);
+
}
diff --git a/src/main/java/com/spotify/reaper/storage/MemoryStorage.java b/src/main/java/com/spotify/reaper/storage/MemoryStorage.java
index 15eb603ca..5838c8bbb 100644
--- a/src/main/java/com/spotify/reaper/storage/MemoryStorage.java
+++ b/src/main/java/com/spotify/reaper/storage/MemoryStorage.java
@@ -85,6 +85,15 @@ public Optional getCluster(String clusterName) {
return Optional.fromNullable(clusters.get(clusterName));
}
+ @Override
+ public Optional deleteCluster(String clusterName) {
+ if (getRepairSchedulesForCluster(clusterName).isEmpty()
+ && getRepairRunsForCluster(clusterName).isEmpty()) {
+ return Optional.fromNullable(clusters.remove(clusterName));
+ }
+ return Optional.absent();
+ }
+
@Override
public RepairRun addRepairRun(RepairRun.Builder repairRun) {
RepairRun newRepairRun = repairRun.build(REPAIR_RUN_ID.incrementAndGet());
@@ -140,6 +149,59 @@ public Collection getRepairRunsWithState(RepairRun.RunState runState)
return foundRepairRuns;
}
+ /**
+ * Delete a RepairUnit instance from Storage, but only if no run or schedule is referencing it.
+ *
+ * @param repairUnitId The RepairUnit instance id to delete.
+ * @return The deleted RepairUnit instance, if delete succeeded.
+ */
+ private Optional deleteRepairUnit(long repairUnitId) {
+ RepairUnit deletedUnit = null;
+ boolean canDelete = true;
+ for (RepairRun repairRun : repairRuns.values()) {
+ if (repairRun.getRepairUnitId() == repairUnitId) {
+ canDelete = false;
+ break;
+ }
+ }
+ if (canDelete) {
+ for (RepairSchedule schedule : repairSchedules.values()) {
+ if (schedule.getRepairUnitId() == repairUnitId) {
+ canDelete = false;
+ break;
+ }
+ }
+ }
+ if (canDelete) {
+ deletedUnit = repairUnits.remove(repairUnitId);
+ repairUnitsByKey.remove(new RepairUnitKey(deletedUnit));
+ }
+ return Optional.fromNullable(deletedUnit);
+ }
+
+ private int deleteRepairSegmentsForRun(long runId) {
+ Map segmentsMap = repairSegmentsByRunId.remove(runId);
+ if (null != segmentsMap) {
+ for (RepairSegment segment : segmentsMap.values()) {
+ repairSegments.remove(segment.getId());
+ }
+ }
+ return segmentsMap != null ? segmentsMap.size() : 0;
+ }
+
+ @Override
+ public Optional deleteRepairRun(long id) {
+ RepairRun deletedRun = repairRuns.remove(id);
+ if (deletedRun != null) {
+ if (getSegmentAmountForRepairRun(id, RepairSegment.State.RUNNING) == 0) {
+ deleteRepairUnit(deletedRun.getRepairUnitId());
+ deleteRepairSegmentsForRun(id);
+ deletedRun = deletedRun.with().runState(RepairRun.RunState.DELETED).build(id);
+ }
+ }
+ return Optional.fromNullable(deletedRun);
+ }
+
@Override
public RepairUnit addRepairUnit(RepairUnit.Builder repairUnit) {
Optional existing =
@@ -149,10 +211,8 @@ public RepairUnit addRepairUnit(RepairUnit.Builder repairUnit) {
} else {
RepairUnit newRepairUnit = repairUnit.build(REPAIR_UNIT_ID.incrementAndGet());
repairUnits.put(newRepairUnit.getId(), newRepairUnit);
- RepairUnitKey unitTables = new RepairUnitKey(newRepairUnit.getClusterName(),
- newRepairUnit.getKeyspaceName(),
- newRepairUnit.getColumnFamilies());
- repairUnitsByKey.put(unitTables, newRepairUnit);
+ RepairUnitKey unitKey = new RepairUnitKey(newRepairUnit);
+ repairUnitsByKey.put(unitKey, newRepairUnit);
return newRepairUnit;
}
}
@@ -164,8 +224,8 @@ public Optional getRepairUnit(long 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)));
}
@Override
@@ -295,12 +355,25 @@ public boolean updateRepairSchedule(RepairSchedule newRepairSchedule) {
}
}
+ @Override
+ public Optional deleteRepairSchedule(long id) {
+ RepairSchedule deletedSchedule = repairSchedules.remove(id);
+ if (deletedSchedule != null) {
+ deletedSchedule = deletedSchedule.with().state(RepairSchedule.State.DELETED).build(id);
+ }
+ return Optional.fromNullable(deletedSchedule);
+ }
+
public static class RepairUnitKey {
public final String cluster;
public final String keyspace;
public final Set tables;
+ public RepairUnitKey(RepairUnit unit) {
+ this(unit.getClusterName(), unit.getKeyspaceName(), unit.getColumnFamilies());
+ }
+
public RepairUnitKey(String cluster, String keyspace, Set tables) {
this.cluster = cluster;
this.keyspace = keyspace;
diff --git a/src/main/java/com/spotify/reaper/storage/PostgresStorage.java b/src/main/java/com/spotify/reaper/storage/PostgresStorage.java
index 2a83f9321..bace13098 100644
--- a/src/main/java/com/spotify/reaper/storage/PostgresStorage.java
+++ b/src/main/java/com/spotify/reaper/storage/PostgresStorage.java
@@ -35,6 +35,7 @@
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;
@@ -86,6 +87,22 @@ public Optional getCluster(String clusterName) {
return Optional.fromNullable(result);
}
+ @Override
+ public Optional deleteCluster(String clusterName) {
+ Cluster result = null;
+ try (Handle h = jdbi.open()) {
+ IStoragePostgreSQL pg = getPostgresStorage(h);
+ Cluster clusterToDel = pg.getCluster(clusterName);
+ if (clusterToDel != null) {
+ int rowsDeleted = pg.deleteCluster(clusterName);
+ if (rowsDeleted > 0) {
+ result = clusterToDel;
+ }
+ }
+ }
+ return Optional.fromNullable(result);
+ }
+
@Override
public boolean isStorageConnected() {
String postgresVersion = null;
@@ -171,6 +188,46 @@ public Collection getRepairRunsWithState(RepairRun.RunState runState)
return result == null ? Lists.newArrayList() : result;
}
+ @Override
+ public Optional deleteRepairRun(long id) {
+ RepairRun result = null;
+ Handle h = null;
+ try {
+ h = jdbi.open();
+ h.begin();
+ IStoragePostgreSQL pg = getPostgresStorage(h);
+ RepairRun runToDelete = pg.getRepairRun(id);
+ if (runToDelete != null) {
+ try {
+ pg.deleteRepairUnit(runToDelete.getRepairUnitId());
+ } catch (DBIException ex) {
+ LOG.info("cannot delete RepairUnit with id " + runToDelete.getRepairUnitId());
+ ex.printStackTrace();
+ }
+ int segmentsRunning = pg.getSegmentAmountForRepairRun(id, RepairSegment.State.RUNNING);
+ if (segmentsRunning == 0) {
+ pg.deleteRepairSegmentsForRun(runToDelete.getId());
+ result = runToDelete.with().runState(RepairRun.RunState.DELETED).build(id);
+ } else {
+ LOG.warn("not deleting RepairRun \"{}\" as it has segments running: {}",
+ id, segmentsRunning);
+ }
+ }
+ h.commit();
+ } catch (DBIException ex) {
+ LOG.warn("DELETE failed", ex);
+ ex.printStackTrace();
+ if (h != null) {
+ h.rollback();
+ }
+ } finally {
+ if (h != null) {
+ h.close();
+ }
+ }
+ return Optional.fromNullable(result);
+ }
+
@Override
public RepairRun addRepairRun(RepairRun.Builder newRepairRun) {
RepairRun result;
@@ -354,4 +411,20 @@ public boolean updateRepairSchedule(RepairSchedule newRepairSchedule) {
}
return result;
}
+
+ @Override
+ public Optional deleteRepairSchedule(long id) {
+ RepairSchedule result = null;
+ try (Handle h = jdbi.open()) {
+ IStoragePostgreSQL pg = getPostgresStorage(h);
+ RepairSchedule scheduleToDel = pg.getRepairSchedule(id);
+ if (scheduleToDel != null) {
+ int rowsDeleted = pg.deleteRepairSchedule(scheduleToDel.getId());
+ if (rowsDeleted > 0) {
+ result = scheduleToDel.with().state(RepairSchedule.State.DELETED).build(id);
+ }
+ }
+ }
+ return Optional.fromNullable(result);
+ }
}
diff --git a/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java b/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java
index 46f5ce5c7..8f1b854af 100644
--- a/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java
+++ b/src/main/java/com/spotify/reaper/storage/postgresql/IStoragePostgreSQL.java
@@ -50,6 +50,7 @@ public interface IStoragePostgreSQL {
+ ") VALUES (:name, :partitioner, :seedHosts)";
static final String SQL_UPDATE_CLUSTER =
"UPDATE cluster SET partitioner = :partitioner, seed_hosts = :seedHosts WHERE name = :name";
+ static final String SQL_DELETE_CLUSTER = "DELETE FROM cluster WHERE name = :name";
// RepairRun
//
@@ -77,6 +78,7 @@ public interface IStoragePostgreSQL {
"SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE state = :state";
static final String SQL_GET_REPAIR_RUNS_FOR_UNIT =
"SELECT " + SQL_REPAIR_RUN_ALL_FIELDS + " FROM repair_run WHERE repair_unit_id = :unitId";
+ static final String SQL_DELETE_REPAIR_RUN = "DELETE FROM repair_run WHERE id = :id";
// RepairUnit
//
@@ -93,6 +95,7 @@ public interface IStoragePostgreSQL {
"SELECT " + SQL_REPAIR_UNIT_ALL_FIELDS + " FROM repair_unit "
+ "WHERE cluster_name = :clusterName AND keyspace_name = :keyspaceName "
+ "AND column_families @> :columnFamilies AND column_families <@ :columnFamilies";
+ static final String SQL_DELETE_REPAIR_UNIT = "DELETE FROM repair_unit WHERE id = :id";
// RepairSegment
//
@@ -122,6 +125,8 @@ public interface IStoragePostgreSQL {
"SELECT " + SQL_REPAIR_SEGMENT_ALL_FIELDS + " FROM repair_segment WHERE "
+ "run_id = :runId AND state = 0 AND start_token >= :startToken "
+ "AND end_token < :endToken ORDER BY fail_count ASC, start_token ASC LIMIT 1";
+ static final String SQL_DELETE_REPAIR_SEGMENTS_FOR_RUN =
+ "DELETE FROM repair_segment WHERE run_id = :runId";
// RepairSchedule
//
@@ -147,6 +152,7 @@ public interface IStoragePostgreSQL {
+ "WHERE repair_schedule.repair_unit_id = repair_unit.id AND cluster_name = :clusterName";
static final String SQL_GET_ALL_REPAIR_SCHEDULES =
"SELECT " + SQL_REPAIR_SCHEDULE_ALL_FIELDS + " FROM repair_schedule";
+ static final String SQL_DELETE_REPAIR_SCHEDULE = "DELETE FROM repair_schedule WHERE id = :id";
// Utility methods
//
@@ -172,6 +178,9 @@ public interface IStoragePostgreSQL {
@SqlUpdate(SQL_UPDATE_CLUSTER)
public int updateCluster(@BindBean Cluster newCluster);
+ @SqlUpdate(SQL_DELETE_CLUSTER)
+ public int deleteCluster(@Bind("name") String clusterName);
+
@SqlQuery(SQL_GET_REPAIR_RUN)
@Mapper(RepairRunMapper.class)
public RepairRun getRepairRun(@Bind("id") long repairRunId);
@@ -195,6 +204,9 @@ public interface IStoragePostgreSQL {
@SqlUpdate(SQL_UPDATE_REPAIR_RUN)
public int updateRepairRun(@BindBean RepairRun newRepairRun);
+ @SqlUpdate(SQL_DELETE_REPAIR_RUN)
+ public int deleteRepairRun(@Bind("id") long repairRunId);
+
@SqlQuery(SQL_GET_REPAIR_UNIT)
@Mapper(RepairUnitMapper.class)
public RepairUnit getRepairUnit(@Bind("id") long repairUnitId);
@@ -209,6 +221,9 @@ public RepairUnit getRepairUnitByClusterAndTables(@Bind("clusterName") String cl
@GetGeneratedKeys
public long insertRepairUnit(@BindBean RepairUnit newRepairUnit);
+ @SqlUpdate(SQL_DELETE_REPAIR_UNIT)
+ public int deleteRepairUnit(@Bind("id") long repairUnitId);
+
@SqlBatch(SQL_INSERT_REPAIR_SEGMENT)
@BatchChunkSize(500)
public void insertRepairSegments(@BindBean Iterator newRepairSegments);
@@ -236,6 +251,9 @@ public RepairSegment getNextFreeRepairSegmentOnRange(@Bind("runId") long runId,
@Bind("startToken") BigInteger startToken,
@Bind("endToken") BigInteger endToken);
+ @SqlUpdate(SQL_DELETE_REPAIR_SEGMENTS_FOR_RUN)
+ public int deleteRepairSegmentsForRun(@Bind("runId") long repairRunId);
+
@SqlQuery(SQL_GET_REPAIR_SCHEDULE)
@Mapper(RepairScheduleMapper.class)
public RepairSchedule getRepairSchedule(@Bind("id") long repairScheduleId);
@@ -260,6 +278,9 @@ public Collection getRepairSchedulesForCluster(
Collection getRepairRunIdsForCluster(
@Bind("clusterName") String clusterName);
+ @SqlUpdate(SQL_DELETE_REPAIR_SCHEDULE)
+ public int deleteRepairSchedule(@Bind("id") long repairScheduleId);
+
@SqlQuery(SQL_SEGMENTS_AMOUNT_FOR_REPAIR_RUN)
int getSegmentAmountForRepairRun(
@Bind("runId") long runId,
diff --git a/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java b/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java
index e392f99ad..e96f93b7d 100644
--- a/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java
+++ b/src/main/java/com/spotify/reaper/storage/postgresql/RepairScheduleMapper.java
@@ -38,9 +38,17 @@ public RepairSchedule map(int index, ResultSet r, StatementContext ctx) throws S
} else {
runHistoryLong = new Long[0];
}
+
+ String stateStr = r.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(
r.getLong("repair_unit_id"),
- RepairSchedule.State.valueOf(r.getString("state")),
+ scheduleState,
r.getInt("days_between"),
RepairRunMapper.getDateTimeOrNull(r, "next_activation"),
ImmutableList.copyOf(runHistoryLong),
diff --git a/src/test/java/com/spotify/reaper/SimpleReaperClient.java b/src/test/java/com/spotify/reaper/SimpleReaperClient.java
new file mode 100644
index 000000000..32ea606b4
--- /dev/null
+++ b/src/test/java/com/spotify/reaper/SimpleReaperClient.java
@@ -0,0 +1,115 @@
+package com.spotify.reaper;
+
+import com.google.common.base.Optional;
+
+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 com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * This is a simple client for testing usage, that calls the Reaper REST API
+ * and turns the resulting JSON into Reaper core entity instances.
+ */
+public class SimpleReaperClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SimpleReaperClient.class);
+
+ private static Optional