Skip to content

Commit

Permalink
add repair run, schedule, and cluster deletion endpoints
Browse files Browse the repository at this point in the history
* refactor acceptance testing
  • Loading branch information
varjoranta committed Feb 26, 2015
1 parent 1f99de9 commit 0c612c3
Show file tree
Hide file tree
Showing 25 changed files with 1,171 additions and 161 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.jackson</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/spotify/reaper/core/RepairRun.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ public enum RunState {
RUNNING,
ERROR,
DONE,
PAUSED
PAUSED,
DELETED
}

public static class Builder {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/spotify/reaper/core/RepairSchedule.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ public Builder with() {
}

public enum State {
RUNNING,
PAUSED
ACTIVE,
PAUSED,
DELETED
}


public static class Builder {

public final long repairUnitId;
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/spotify/reaper/resources/ClusterResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Cluster> 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<Cluster> 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();
}

}
10 changes: 8 additions & 2 deletions src/main/java/com/spotify/reaper/resources/CommonTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ private static List<RingRange> generateSegments(AppContext context, Cluster targ
int segmentCount)
throws ReaperException {
List<RingRange> segments = null;
assert targetCluster.getPartitioner() != null :
"no partitioner for cluster: " + targetCluster.getName();
SegmentGenerator sg = new SegmentGenerator(targetCluster.getPartitioner());
Set<String> seedHosts = targetCluster.getSeedHosts();
if (seedHosts.isEmpty()) {
Expand Down Expand Up @@ -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.<Long>of(), segments, repairParallelism, intensity,
new RepairSchedule.Builder(repairUnit.getId(), RepairSchedule.State.ACTIVE, daysBetween,
nextActivation, ImmutableList.<Long>of(), segments,
repairParallelism, intensity,
DateTime.now());
scheduleBuilder.owner(owner);
RepairSchedule newRepairSchedule = context.storage.addRepairSchedule(scheduleBuilder);
Expand Down Expand Up @@ -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);
}

Expand Down
63 changes: 61 additions & 2 deletions src/main/java/com/spotify/reaper/resources/RepairRunResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -401,7 +408,7 @@ public Set splitStateParam(Optional<String> state) {
Iterable<String> 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;
Expand All @@ -413,4 +420,56 @@ public Set splitStateParam(Optional<String> 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<String> 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<RepairRun> 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<RepairUnit> unitPossiblyDeleted =
context.storage.getRepairUnit(runToDelete.get().getRepairUnitId());
Optional<RepairRun> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -170,11 +171,11 @@ public Response addRepairSchedule(
}

/**
* Modifies a state of the repair schedule. <p/> Currently supports PAUSED -> RUNNING and
* RUNNING -> PAUSED.
* Modifies a state of the repair schedule. <p/> 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}")
Expand All @@ -184,7 +185,7 @@ public Response modifyState(
@QueryParam("state") Optional<String> 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())
Expand All @@ -194,7 +195,7 @@ public Response modifyState(
Optional<RepairSchedule> 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();
}

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<String> 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<RepairSchedule> 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<RepairSchedule> 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();
}

}
Loading

0 comments on commit 0c612c3

Please sign in to comment.