Java tutorial
/* * 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 io.cassandrareaper.resources; import io.cassandrareaper.AppContext; import io.cassandrareaper.ReaperException; import io.cassandrareaper.core.Cluster; import io.cassandrareaper.core.Node; import io.cassandrareaper.core.RepairRun; import io.cassandrareaper.core.RepairRun.RunState; import io.cassandrareaper.core.RepairSegment; import io.cassandrareaper.core.RepairUnit; import io.cassandrareaper.jmx.JmxProxy; import io.cassandrareaper.resources.view.RepairRunStatus; import io.cassandrareaper.service.RepairRunService; import io.cassandrareaper.service.RepairUnitService; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.validation.ValidationException; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; 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.apache.cassandra.repair.RepairParallelism; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Path("/repair_run") @Produces(MediaType.APPLICATION_JSON) public final class RepairRunResource { private static final Logger LOG = LoggerFactory.getLogger(RepairRunResource.class); private final AppContext context; private final RepairUnitService repairUnitService; private final RepairRunService repairRunService; public RepairRunResource(AppContext context) { this.context = context; this.repairUnitService = RepairUnitService.create(context); this.repairRunService = RepairRunService.create(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. * * <p>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. */ @POST public Response addRepairRun(@Context UriInfo uriInfo, @QueryParam("clusterName") Optional<String> clusterName, @QueryParam("keyspace") Optional<String> keyspace, @QueryParam("tables") Optional<String> tableNamesParam, @QueryParam("owner") Optional<String> owner, @QueryParam("cause") Optional<String> cause, @QueryParam("segmentCount") Optional<Integer> segmentCountPerNode, @QueryParam("repairParallelism") Optional<String> repairParallelism, @QueryParam("intensity") Optional<String> intensityStr, @QueryParam("incrementalRepair") Optional<String> incrementalRepairStr, @QueryParam("nodes") Optional<String> nodesToRepairParam, @QueryParam("datacenters") Optional<String> datacentersToRepairParam, @QueryParam("blacklistedTables") Optional<String> blacklistedTableNamesParam, @QueryParam("repairThreadCount") Optional<Integer> repairThreadCountParam) { try { final Response possibleFailedResponse = RepairRunResource.checkRequestForAddRepair(context, clusterName, keyspace, owner, segmentCountPerNode, repairParallelism, intensityStr, incrementalRepairStr, nodesToRepairParam, datacentersToRepairParam, repairThreadCountParam); if (null != possibleFailedResponse) { return possibleFailedResponse; } 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); } 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); } int segments = context.config.getSegmentCountPerNode(); if (!incrementalRepair) { if (segmentCountPerNode.isPresent()) { LOG.debug("using given segment count {} instead of configured value {}", segmentCountPerNode.get(), context.config.getSegmentCount()); segments = segmentCountPerNode.get(); } } else { // hijack the segment count in case of incremental repair segments = -1; } final Cluster cluster = context.storage.getCluster(Cluster.toSymbolicName(clusterName.get())).get(); Set<String> tableNames; try { tableNames = repairRunService.getTableNamesBasedOnParam(cluster, keyspace.get(), tableNamesParam); } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } Set<String> blacklistedTableNames; try { blacklistedTableNames = repairRunService.getTableNamesBasedOnParam(cluster, keyspace.get(), blacklistedTableNamesParam); } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } final Set<String> nodesToRepair; try { nodesToRepair = repairRunService.getNodesToRepairBasedOnParam(cluster, nodesToRepairParam); } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } final Set<String> datacentersToRepair; try { datacentersToRepair = RepairRunService.getDatacentersToRepairBasedOnParam(cluster, datacentersToRepairParam); } catch (IllegalArgumentException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build(); } RepairUnit.Builder builder = new RepairUnit.Builder(cluster.getName(), keyspace.get(), tableNames, incrementalRepair, nodesToRepair, datacentersToRepair, blacklistedTableNames, repairThreadCountParam.or(context.config.getRepairThreadCount())); RepairUnit theRepairUnit = repairUnitService.getNewOrExistingRepairUnit(cluster, builder); if (theRepairUnit.getIncrementalRepair() != incrementalRepair) { String msg = String.format("A repair unit %s already exist for the same cluster/keyspace/tables" + " but with a different incremental repair value. Requested value %s | Existing value: %s", theRepairUnit.getId(), incrementalRepair, theRepairUnit.getIncrementalRepair()); return Response.status(Response.Status.BAD_REQUEST).entity(msg).build(); } RepairParallelism parallelism = context.config.getRepairParallelism(); if (repairParallelism.isPresent()) { LOG.debug("using given repair parallelism {} instead of configured value {}", repairParallelism.get(), context.config.getRepairParallelism()); parallelism = RepairParallelism.valueOf(repairParallelism.get().toUpperCase()); } if (incrementalRepair) { parallelism = RepairParallelism.PARALLEL; } final RepairRun newRepairRun = repairRunService.registerRepairRun(cluster, theRepairUnit, cause, owner.get(), 0, segments, parallelism, intensity); return Response.created(buildRepairRunUri(uriInfo, newRepairRun)) .entity(new RepairRunStatus(newRepairRun, theRepairUnit, 0)).build(); } catch (ReaperException e) { LOG.error(e.getMessage(), e); return Response.serverError().entity(e.getMessage()).build(); } } /** * @return Response instance in case there is a problem, or null if everything is ok. **/ @Nullable static Response checkRequestForAddRepair(AppContext context, Optional<String> clusterName, Optional<String> keyspace, Optional<String> owner, Optional<Integer> segmentCountPerNode, Optional<String> repairParallelism, Optional<String> intensityStr, Optional<String> incrementalRepairStr, Optional<String> nodesStr, Optional<String> datacentersStr, Optional<Integer> repairThreadCountStr) { if (!clusterName.isPresent()) { return createMissingArgumentResponse("clusterName"); } if (!keyspace.isPresent()) { return createMissingArgumentResponse("keyspace"); } if (!owner.isPresent()) { return createMissingArgumentResponse("owner"); } if (segmentCountPerNode.isPresent() && (segmentCountPerNode.get() < 0 || segmentCountPerNode.get() > 1000)) { return Response.status(Response.Status.BAD_REQUEST) .entity("invalid query parameter \"segmentCountPerNode\", maximum value is 100000").build(); } if (repairParallelism.isPresent()) { try { checkRepairParallelismString(repairParallelism.get()); } catch (ReaperException ex) { LOG.error(ex.getMessage(), ex); return Response.status(Response.Status.BAD_REQUEST).entity(ex.getMessage()).build(); } } if (intensityStr.isPresent()) { try { // @todo all BAD_REQUEST responses should be instead thrown ValidationExceptions, so this method returns void parseIntensity(intensityStr.get()); } catch (ValidationException ex) { return Response.status(Status.BAD_REQUEST).entity(ex.getMessage()).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> 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(); } if (!datacentersStr.or("").isEmpty() && !nodesStr.or("").isEmpty()) { return Response.status(Response.Status.BAD_REQUEST).entity( "Parameters \"datacenters\" and \"nodes\" are mutually exclusive. Please fill just one between the two.") .build(); } if (incrementalRepairStr.isPresent() && "true".equalsIgnoreCase(incrementalRepairStr.get())) { try { JmxProxy jmxProxy = context.jmxConnectionFactory.connectAny(Optional.absent(), cluster.get().getSeedHosts().stream() .map(host -> Node.builder().withCluster(cluster.get()).withHostname(host).build()) .collect(Collectors.toList()), context.config.getJmxConnectionTimeoutInSeconds()); String version = jmxProxy.getCassandraVersion(); if (null != version && version.startsWith("2.0")) { String msg = "Incremental repair does not work with Cassandra versions before 2.1"; return Response.status(Response.Status.BAD_REQUEST).entity(msg).build(); } } catch (ReaperException e) { String msg = String.format("find version of cluster %s failed", cluster.get().getName()); LOG.error(msg, e); return Response.serverError().entity(msg).build(); } } return null; } /** * Modifies a state of the repair run. * * <p>Currently supports NOT_STARTED|PAUSED to RUNNING and RUNNING to PAUSED. * * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 409 * (CONFLICT) if transition is not supported. */ @PUT @Path("/{id}/state/{state}") public Response modifyRunState(@Context UriInfo uriInfo, @PathParam("id") UUID repairRunId, @PathParam("state") Optional<String> stateStr) throws ReaperException { LOG.info("modify repair run state called with: id = {}, state = {}", repairRunId, stateStr); try { if (!stateStr.isPresent()) { return createMissingArgumentResponse("state"); } Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (!repairRun.isPresent()) { return Response.status(Status.NOT_FOUND).entity("repair run " + repairRunId + " doesn't exist") .build(); } final RepairRun.RunState newState = parseRunState(stateStr.get()); if (isUnitAlreadyRepairing(repairRun.get())) { String errMsg = "repair unit already has run " + repairRun.get().getRepairUnitId() + " in RUNNING state"; LOG.error(errMsg); return Response.status(Status.CONFLICT).entity(errMsg).build(); } final RunState oldState = repairRun.get().getRunState(); if (oldState == newState) { String msg = "given \"state\" " + stateStr + " is same as the current run state"; return Response.noContent().entity(msg).location(buildRepairRunUri(uriInfo, repairRun.get())) .build(); } if (isStarting(oldState, newState)) { return startRun(uriInfo, repairRun.get()); } else if (isPausing(oldState, newState)) { return pauseRun(uriInfo, repairRun.get()); } else if (isResuming(oldState, newState) || isRetrying(oldState, newState)) { return resumeRun(uriInfo, repairRun.get()); } else if (isAborting(oldState, newState)) { return abortRun(uriInfo, repairRun.get()); } else { String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(), newState.toString()); LOG.error(errMsg); return Response.status(Status.CONFLICT).entity(errMsg).build(); } } catch (ValidationException ex) { return Response.status(Status.BAD_REQUEST).entity(ex.getMessage()).build(); } } /** * Modifies the intensity of the repair run. * * @return OK if all goes well NOT_MODIFIED if new state is the same as the old one, and 409 * (CONFLICT) if transition is not supported. */ @PUT @Path("/{id}/intensity/{intensity}") public Response modifyRunIntensity(@Context UriInfo uriInfo, @PathParam("id") UUID repairRunId, @PathParam("intensity") Optional<String> intensityStr) throws ReaperException { LOG.info("modify repair run intensity called with: id = {}, state = {}", repairRunId, intensityStr); try { if (!intensityStr.isPresent()) { return createMissingArgumentResponse("intensity"); } final double intensity = parseIntensity(intensityStr.get()); Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (!repairRun.isPresent()) { return Response.status(Status.NOT_FOUND).entity("repair run " + repairRunId + " doesn't exist") .build(); } if (RunState.PAUSED != repairRun.get().getRunState() && RunState.NOT_STARTED != repairRun.get().getRunState()) { return Response.status(Status.CONFLICT).entity("repair run must first be paused").build(); } return updateRunIntensity(uriInfo, repairRun.get(), intensity); } catch (ValidationException ex) { return Response.status(Response.Status.BAD_REQUEST).entity(ex.getMessage()).build(); } } /** * MOVED_PERMANENTLY to PUT repair_run/{id}/state/{state} */ @PUT @Path("/{id}") @Deprecated public Response oldModifyRunState(@Context UriInfo uriInfo, @PathParam("id") UUID repairRunId, @QueryParam("state") Optional<String> stateStr) throws ReaperException { try { if (!stateStr.isPresent()) { return createMissingArgumentResponse("state"); } RepairRun.RunState state = parseRunState(stateStr.get()); Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (!repairRun.isPresent()) { return Response.status(Status.NOT_FOUND).entity("repair run " + repairRunId + " doesn't exist") .build(); } URI redirectUri = uriInfo.getRequestUriBuilder() .replacePath(String.format("repair_run/%s/state/%s", repairRun.get().getId().toString(), state)) .replaceQuery("").build(); return Response.seeOther(redirectUri).build(); } catch (ValidationException ex) { return Response.status(Status.BAD_REQUEST).entity(ex.getMessage()).build(); } } private static boolean isStarting(RepairRun.RunState oldState, RepairRun.RunState newState) { return oldState == RepairRun.RunState.NOT_STARTED && newState == RepairRun.RunState.RUNNING; } private static boolean isPausing(RepairRun.RunState oldState, RepairRun.RunState newState) { return oldState == RepairRun.RunState.RUNNING && newState == RepairRun.RunState.PAUSED; } private static boolean isResuming(RepairRun.RunState oldState, RepairRun.RunState newState) { return oldState == RepairRun.RunState.PAUSED && newState == RepairRun.RunState.RUNNING; } private static boolean isRetrying(RepairRun.RunState oldState, RepairRun.RunState newState) { return oldState == RepairRun.RunState.ERROR && newState == RepairRun.RunState.RUNNING; } private static boolean isAborting(RepairRun.RunState oldState, RepairRun.RunState newState) { return oldState != RepairRun.RunState.ERROR && newState == RepairRun.RunState.ABORTED; } private boolean isUnitAlreadyRepairing(RepairRun repairRun) { return context.storage.getRepairRunsForUnit(repairRun.getRepairUnitId()).stream().anyMatch( (run) -> (!run.getId().equals(repairRun.getId()) && run.getRunState().equals(RunState.RUNNING))); } private int getSegmentAmountForRepairRun(UUID repairRunId) { return context.storage.getSegmentAmountForRepairRunWithState(repairRunId, RepairSegment.State.DONE); } private Response startRun(UriInfo uriInfo, RepairRun repairRun) throws ReaperException { LOG.info("Starting run {}", repairRun.getId()); final RepairRun newRun = context.repairManager.startRepairRun(repairRun); return Response.ok().location(buildRepairRunUri(uriInfo, newRun)).build(); } private Response pauseRun(UriInfo uriInfo, RepairRun repairRun) throws ReaperException { LOG.info("Pausing run {}", repairRun.getId()); final RepairRun newRun = context.repairManager.pauseRepairRun(repairRun); return Response.ok().location(buildRepairRunUri(uriInfo, newRun)).build(); } private Response resumeRun(UriInfo uriInfo, RepairRun repairRun) throws ReaperException { LOG.info("Resuming run {}", repairRun.getId()); final RepairRun newRun = context.repairManager.startRepairRun(repairRun); return Response.ok().location(buildRepairRunUri(uriInfo, newRun)).build(); } private Response abortRun(UriInfo uriInfo, RepairRun repairRun) throws ReaperException { LOG.info("Aborting run {}", repairRun.getId()); final RepairRun newRun = context.repairManager.abortRepairRun(repairRun); return Response.ok().location(buildRepairRunUri(uriInfo, newRun)).build(); } private Response updateRunIntensity(UriInfo uriInfo, RepairRun run, double intensity) throws ReaperException { LOG.info("Editing run {}", run.getId()); RepairRun newRun = context.repairManager.updateRepairRunIntensity(run, intensity); return Response.ok().location(buildRepairRunUri(uriInfo, newRun)).build(); } /** * @return detailed information about a repair run. */ @GET @Path("/{id}") public Response getRepairRun(@PathParam("id") UUID repairRunId) { LOG.debug("get repair_run called with: id = {}", repairRunId); final Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (repairRun.isPresent()) { RepairRunStatus repairRunStatus = getRepairRunStatus(repairRun.get()); return Response.ok().entity(repairRunStatus).build(); } else { return Response.status(404).entity("repair run " + repairRunId + " doesn't exist").build(); } } /** * @return list the segments of a repair run. */ @GET @Path("/{id}/segments") public Response getRepairRunSegments(@PathParam("id") UUID repairRunId) { LOG.debug("get repair_run called with: id = {}", repairRunId); final Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (repairRun.isPresent()) { Collection<RepairSegment> segments = context.storage.getRepairSegmentsForRun(repairRunId); return Response.ok().entity(segments).build(); } else { return Response.status(404).entity("repair run " + repairRunId + " doesn't exist").build(); } } /** * @return Aborts a running segment. */ @GET @Path("/{id}/segments/abort/{segment_id}") public Response getRepairRunSegments(@PathParam("id") UUID repairRunId, @PathParam("segment_id") UUID segmentId) { LOG.debug("abort segment called with: run id = {} and segment id = {}", repairRunId, segmentId); final Optional<RepairRun> repairRun = context.storage.getRepairRun(repairRunId); if (repairRun.isPresent()) { if (RepairRun.RunState.RUNNING == repairRun.get().getRunState() || RepairRun.RunState.PAUSED == repairRun.get().getRunState()) { RepairSegment segment = context.repairManager.abortSegment(repairRunId, segmentId); return Response.ok().entity(segment).build(); } else { return Response.status(Response.Status.CONFLICT) .entity("Cannot abort segment on repair run with status " + repairRun.get().getRunState()) .build(); } } else { return Response.status(404).entity("repair run " + repairRunId + " doesn't exist").build(); } } /** * @return all know repair runs for a cluster. */ @GET @Path("/cluster/{cluster_name}") public Response getRepairRunsForCluster(@PathParam("cluster_name") String clusterName) { LOG.debug("get repair run for cluster called with: cluster_name = {}", clusterName); final Collection<RepairRun> repairRuns = context.storage.getRepairRunsForCluster(clusterName, Optional.absent()); final Collection<RepairRunStatus> repairRunViews = new ArrayList<>(); for (final RepairRun repairRun : repairRuns) { repairRunViews.add(getRepairRunStatus(repairRun)); } return Response.ok().entity(repairRunViews).build(); } /** * @return only a status of a repair run, not the entire repair run info. */ private RepairRunStatus getRepairRunStatus(RepairRun repairRun) { RepairUnit repairUnit = context.storage.getRepairUnit(repairRun.getRepairUnitId()); int segmentsRepaired = getSegmentAmountForRepairRun(repairRun.getId()); return new RepairRunStatus(repairRun, repairUnit, segmentsRepaired); } /** * Crafts an URI used to identify given repair run. * * @return The created resource URI. */ private static URI buildRepairRunUri(UriInfo uriInfo, RepairRun repairRun) { return uriInfo.getBaseUriBuilder().path("repair_run").path(repairRun.getId().toString()).build(); } /** * @param state comma-separated list of states to return. These states must match names of {@link * io.cassandrareaper.core.RepairRun.RunState}. * @param cluster only return repair runs belonging to this cluster * @param keyspace only return repair runs belonging to this keyspace * @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<String> state, @QueryParam("cluster_name") Optional<String> cluster, @QueryParam("keyspace_name") Optional<String> keyspace) { try { final List<RepairRunStatus> runStatuses = Lists.newArrayList(); final Set desiredStates = splitStateParam(state); if (desiredStates == null) { return Response.status(Response.Status.BAD_REQUEST).build(); } Collection<Cluster> clusters = cluster.transform((clstr) -> context.storage.getCluster(clstr).get()) .transform((clstr) -> (Collection<Cluster>) Collections.singleton(clstr)) .or(context.storage.getClusters()); for (final Cluster clstr : clusters) { Collection<RepairRun> runs = context.storage.getRepairRunsForCluster(clstr.getName(), Optional.absent()); runStatuses.addAll((List<RepairRunStatus>) getRunStatuses(runs, desiredStates).stream() .filter((run) -> !keyspace.isPresent() || ((RepairRunStatus) run).getKeyspaceName().equals(keyspace.get())) .collect(Collectors.toList())); } return Response.ok().entity(runStatuses).build(); } catch (ReaperException e) { LOG.error("Failed listing cluster statuses", e); return Response.serverError().entity("Failed listing cluster statuses").build(); } } private List<RepairRunStatus> getRunStatuses(Collection<RepairRun> runs, Set<String> desiredStates) throws ReaperException { final List<RepairRunStatus> runStatuses = Lists.newArrayList(); for (final RepairRun run : runs) { if (!desiredStates.isEmpty() && !desiredStates.contains(run.getRunState().name())) { continue; } RepairUnit runsUnit = context.storage.getRepairUnit(run.getRepairUnitId()); int segmentsRepaired = run.getSegmentCount(); if (!run.getRunState().equals(RepairRun.RunState.DONE)) { segmentsRepaired = getSegmentAmountForRepairRun(run.getId()); } runStatuses.add(new RepairRunStatus(run, runsUnit, segmentsRepaired)); } return runStatuses; } static Set splitStateParam(Optional<String> state) { if (state.isPresent()) { final Iterable<String> chunks = RepairRunService.COMMA_SEPARATED_LIST_SPLITTER.split(state.get()); for (final String chunk : chunks) { try { RepairRun.RunState.valueOf(chunk.toUpperCase()); } catch (IllegalArgumentException e) { LOG.warn("Listing repair runs called with erroneous states: {}", state.get(), e); return null; } } return Sets.newHashSet(chunks); } else { return Sets.newHashSet(); } } /** * Delete a RepairRun object with given id. * * <p> * 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. * @return The deleted RepairRun instance, with state overwritten to string "DELETED". */ @DELETE @Path("/{id}") public Response deleteRepairRun(@PathParam("id") UUID 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(); } final Optional<RepairRun> runToDelete = context.storage.getRepairRun(runId); if (runToDelete.isPresent()) { if (RepairRun.RunState.RUNNING == runToDelete.get().getRunState()) { return Response.status(Response.Status.CONFLICT).entity("Repair run with id \"" + runId + "\" is currently running, and must be stopped before deleting").build(); } if (!runToDelete.get().getOwner().equalsIgnoreCase(owner.get())) { String msg = String.format("Repair run %s is not owned by the user you defined %s", runId, owner.get()); return Response.status(Response.Status.CONFLICT).entity(msg).build(); } if (context.storage.getSegmentAmountForRepairRunWithState(runId, RepairSegment.State.RUNNING) > 0) { String msg = String.format("Repair run %s has running segments, which must finish before deleting", runId); return Response.status(Response.Status.CONFLICT).entity(msg).build(); } context.storage.deleteRepairRun(runId); return Response.accepted().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 %s" + runId + " not found").build(); } private static void checkRepairParallelismString(String repairParallelism) throws ReaperException { try { RepairParallelism.valueOf(repairParallelism.toUpperCase()); } catch (IllegalArgumentException ex) { throw new ReaperException("invalid repair parallelism given \"" + repairParallelism + "\", must be one of: " + Arrays.toString(RepairParallelism.values()), ex); } } private static Response createMissingArgumentResponse(String argumentName) { return Response.status(Status.BAD_REQUEST).entity(argumentName + " argument missing").build(); } private static RunState parseRunState(String input) throws ValidationException { try { return RunState.valueOf(input.toUpperCase()); } catch (IllegalArgumentException ex) { throw new ValidationException("invalid \"state\" argument: " + input, ex); } } private static double parseIntensity(String input) throws ValidationException { try { double intensity = Double.parseDouble(input); if (intensity <= 0.0 || intensity > 1.0) { throw new ValidationException( "query parameter \"intensity\" must be in range (0.0, 1.0]: " + input); } return intensity; } catch (NumberFormatException ex) { throw new ValidationException("invalid value for query parameter \"intensity\": " + input, ex); } } }