io.cassandrareaper.resources.RepairScheduleResource.java Source code

Java tutorial

Introduction

Here is the source code for io.cassandrareaper.resources.RepairScheduleResource.java

Source

/*
 * 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.RepairSchedule;
import io.cassandrareaper.core.RepairUnit;
import io.cassandrareaper.resources.view.RepairRunStatus;
import io.cassandrareaper.resources.view.RepairScheduleStatus;
import io.cassandrareaper.service.RepairRunService;
import io.cassandrareaper.service.RepairScheduleService;
import io.cassandrareaper.service.RepairUnitService;
import io.cassandrareaper.service.SchedulingManager;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

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.UriInfo;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.cassandra.repair.RepairParallelism;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/repair_schedule")
@Produces(MediaType.APPLICATION_JSON)
public final class RepairScheduleResource {

    private static final Logger LOG = LoggerFactory.getLogger(RepairScheduleResource.class);

    private final AppContext context;
    private final RepairUnitService repairUnitService;
    private final RepairScheduleService repairScheduleService;
    private final RepairRunService repairRunService;

    public RepairScheduleResource(AppContext context) {
        this.context = context;
        this.repairUnitService = RepairUnitService.create(context);
        this.repairScheduleService = RepairScheduleService.create(context);
        this.repairRunService = RepairRunService.create(context);
    }

    /**
     * Endpoint used to create a repair schedule. Does not allow triggering the run. Repair schedule
     * will create new repair runs based on the schedule.
     *
     * <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 runs will target all the tables in the keyspace.
     *
     * @return created repair schedule data as JSON.
     */
    @POST
    public Response addRepairSchedule(@Context UriInfo uriInfo,
            @QueryParam("clusterName") Optional<String> clusterName,
            @QueryParam("keyspace") Optional<String> keyspace,
            @QueryParam("tables") Optional<String> tableNamesParam, @QueryParam("owner") Optional<String> owner,
            @QueryParam("segmentCountPerNode") Optional<Integer> segmentCountPerNode,
            @QueryParam("repairParallelism") Optional<String> repairParallelism,
            @QueryParam("intensity") Optional<String> intensityStr,
            @QueryParam("incrementalRepair") Optional<String> incrementalRepairStr,
            @QueryParam("scheduleDaysBetween") Optional<Integer> scheduleDaysBetween,
            @QueryParam("scheduleTriggerTime") Optional<String> scheduleTriggerTime,
            @QueryParam("nodes") Optional<String> nodesToRepairParam,
            @QueryParam("datacenters") Optional<String> datacentersToRepairParam,
            @QueryParam("blacklistedTables") Optional<String> blacklistedTableNamesParam,
            @QueryParam("repairThreadCount") Optional<Integer> repairThreadCountParam) {

        try {
            Response possibleFailResponse = RepairRunResource.checkRequestForAddRepair(context, clusterName,
                    keyspace, owner, segmentCountPerNode, repairParallelism, intensityStr, incrementalRepairStr,
                    nodesToRepairParam, datacentersToRepairParam, repairThreadCountParam);

            if (null != possibleFailResponse) {
                return possibleFailResponse;
            }

            DateTime nextActivation;
            try {
                nextActivation = getNextActivationTime(scheduleTriggerTime);
                if (nextActivation.isBefore(DateTime.now().minusMinutes(15))) {
                    return Response.status(Response.Status.BAD_REQUEST)
                            .entity("given schedule_trigger_time is too far in the past: "
                                    + RepairRunStatus.dateTimeToIso8601(nextActivation))
                            .build();
                }
            } catch (IllegalArgumentException ex) {
                LOG.info("cannot parse data string: {}", scheduleTriggerTime.get(), ex);
                return Response.status(Response.Status.BAD_REQUEST).entity("invalid schedule_trigger_time").build();
            }

            if (!scheduleDaysBetween.isPresent()) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity("missing required parameter: scheduleDaysBetween").build();
            }

            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 (final 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 (final IllegalArgumentException ex) {
                LOG.error(ex.getMessage(), ex);
                return Response.status(Response.Status.NOT_FOUND).entity(ex.getMessage()).build();
            }

            boolean incremental = isIncrementalRepair(incrementalRepairStr);
            RepairParallelism parallelism = context.config.getRepairParallelism();
            if (repairParallelism.isPresent()) {
                LOG.debug("using given repair parallelism {} over configured value {}", repairParallelism.get(),
                        parallelism);
                parallelism = RepairParallelism.valueOf(repairParallelism.get().toUpperCase());
            }

            if (!parallelism.equals(RepairParallelism.PARALLEL) && incremental) {
                return Response.status(Response.Status.BAD_REQUEST)
                        .entity("Can't mix sequential repair and incremental repairs").build();
            }

            RepairUnit.Builder unitBuilder = RepairUnit.builder().clusterName(cluster.getName())
                    .keyspaceName(keyspace.get()).columnFamilies(tableNames).incrementalRepair(incremental)
                    .nodes(nodesToRepair).datacenters(datacentersToRepair).blacklistedTables(blacklistedTableNames)
                    .repairThreadCount(repairThreadCountParam.orElse(context.config.getRepairThreadCount()));

            return addRepairSchedule(cluster, unitBuilder, getDaysBetween(scheduleDaysBetween), owner.get(),
                    parallelism, uriInfo, incremental, nextActivation, getSegmentCount(segmentCountPerNode),
                    getIntensity(intensityStr));

        } catch (ReaperException e) {
            LOG.error(e.getMessage(), e);
            return Response.serverError().entity(e.getMessage()).build();
        }
    }

    private Response addRepairSchedule(Cluster cluster, RepairUnit.Builder unitBuilder, int days, String owner,
            RepairParallelism parallel, UriInfo uriInfo, boolean incremental, DateTime next, int segments,
            Double intensity) {

        Optional<RepairSchedule> conflictingRepairSchedule = repairScheduleService
                .conflictingRepairSchedule(cluster, unitBuilder);

        if (conflictingRepairSchedule.isPresent()) {
            RepairSchedule existingSchedule = conflictingRepairSchedule.get();

            if (existingSchedule.getDaysBetween() == days && existingSchedule.getOwner().equals(owner)
                    && existingSchedule.getRepairParallelism() == parallel) {

                return Response.noContent().location(buildRepairScheduleUri(uriInfo, existingSchedule)).build();
            }

            String msg = String.format(
                    "A repair schedule already exists for cluster \"%s\", keyspace \"%s\", and column families: %s",
                    cluster.getName(), unitBuilder.keyspaceName, unitBuilder.columnFamilies);

            return Response.status(Response.Status.CONFLICT)
                    .location(buildRepairScheduleUri(uriInfo, existingSchedule)).entity(msg).build();
        } else {

            RepairUnit unit = repairUnitService.getOrCreateRepairUnit(cluster, unitBuilder);

            Preconditions.checkState(unit.getIncrementalRepair() == incremental, "%s!=%s",
                    unit.getIncrementalRepair(), incremental);

            RepairSchedule newRepairSchedule = repairScheduleService.storeNewRepairSchedule(cluster, unit, days,
                    next, owner, segments, parallel, intensity);

            return Response.created(buildRepairScheduleUri(uriInfo, newRepairSchedule)).build();
        }
    }

    private int getDaysBetween(Optional<Integer> scheduleDaysBetween) {
        int daysBetween = context.config.getScheduleDaysBetween();
        if (scheduleDaysBetween.isPresent()) {
            LOG.debug("using given schedule days between {} instead of configured value {}",
                    scheduleDaysBetween.get(), context.config.getScheduleDaysBetween());
            daysBetween = scheduleDaysBetween.get();
        }
        return daysBetween;
    }

    private int getSegmentCount(Optional<Integer> segmentCount) {
        int segments = 0;
        if (segmentCount.isPresent()) {
            LOG.debug("using given segment count {}", segmentCount.get());
            segments = segmentCount.get();
        }
        return segments;
    }

    private Boolean isIncrementalRepair(Optional<String> incrementalRepairStr) {
        Boolean incrementalRepair;
        if (incrementalRepairStr.isPresent()) {
            incrementalRepair = Boolean.parseBoolean(incrementalRepairStr.get());
        } else {
            incrementalRepair = context.config.getIncrementalRepair();
            LOG.debug("no incremental repair given, so using default value: {}", incrementalRepair);
        }
        return incrementalRepair;
    }

    private Double getIntensity(Optional<String> intensityStr) throws NumberFormatException {
        Double intensity;
        if (intensityStr.isPresent()) {
            intensity = Double.parseDouble(intensityStr.get());
        } else {
            intensity = context.config.getRepairIntensity();
            LOG.debug("no intensity given, so using default value: {}", intensity);
        }
        return intensity;
    }

    /**
     * Modifies a state of the repair schedule.
     *
     * <p>Currently supports PAUSED to ACTIVE and ACTIVE to PAUSED.
     *
     * @return OK if all goes well, NO_CONTENT if new state is the same as the old one,
     *     and 400 (BAD_REQUEST) if transition is not supported.
     */
    @PUT
    @Path("/{id}")
    public Response modifyState(@Context UriInfo uriInfo, @PathParam("id") UUID repairScheduleId,
            @QueryParam("state") Optional<String> state) {

        LOG.info("modify repair schedule state called with: id = {}, state = {}", repairScheduleId, state);

        if (!state.isPresent()) {
            return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).entity("\"state\" argument missing")
                    .build();
        }

        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").build();
        }

        RepairUnit repairUnit = context.storage.getRepairUnit(repairSchedule.get().getRepairUnitId());
        RepairSchedule.State newState;
        try {
            newState = RepairSchedule.State.valueOf(state.get().toUpperCase());
        } catch (IllegalArgumentException ex) {
            LOG.error(ex.getMessage(), 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) {
            return Response.noContent().location(buildRepairScheduleUri(uriInfo, repairSchedule.get())).build();
        }

        if (isPausing(oldState, newState)) {
            return pauseSchedule(repairSchedule.get(), uriInfo);
        } else if (isResuming(oldState, newState)) {
            return resumeSchedule(repairSchedule.get(), uriInfo);
        } else {
            String errMsg = String.format("Transition %s->%s not supported.", oldState.toString(),
                    newState.toString());
            LOG.error(errMsg);
            return Response.status(Response.Status.BAD_REQUEST).entity(errMsg).build();
        }
    }

    private static boolean isPausing(RepairSchedule.State oldState, RepairSchedule.State newState) {
        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.ACTIVE;
    }

    private Response pauseSchedule(RepairSchedule repairSchedule, UriInfo uriInfo) {
        LOG.info("Pausing schedule {}", repairSchedule.getId());
        SchedulingManager.pauseRepairSchedule(context, repairSchedule);
        return Response.ok().location(buildRepairScheduleUri(uriInfo, repairSchedule)).build();
    }

    private Response resumeSchedule(RepairSchedule repairSchedule, UriInfo uriInfo) {
        LOG.info("Resuming schedule {}", repairSchedule.getId());
        SchedulingManager.resumeRepairSchedule(context, repairSchedule);
        return Response.ok().location(buildRepairScheduleUri(uriInfo, repairSchedule)).build();
    }

    /**
     * @return detailed information about a repair schedule.
     */
    @GET
    @Path("/{id}")
    public Response getRepairSchedule(@PathParam("id") UUID repairScheduleId) {
        LOG.debug("get repair_schedule called with: id = {}", repairScheduleId);
        Optional<RepairSchedule> repairSchedule = context.storage.getRepairSchedule(repairScheduleId);
        if (repairSchedule.isPresent()) {
            return Response.ok().entity(getRepairScheduleStatus(repairSchedule.get())).build();
        } else {
            return Response.status(404).entity("repair schedule with id " + repairScheduleId + " doesn't exist")
                    .build();
        }
    }

    /**
     * Force start a repair from a schedule.
     *
     * @return detailed information about a repair schedule.
     */
    @POST
    @Path("/start/{id}")
    public Response startRepairSchedule(@PathParam("id") UUID repairScheduleId) {
        LOG.debug("start repair_schedule called with: id = {}", repairScheduleId);
        Optional<RepairSchedule> repairSchedule = context.storage.getRepairSchedule(repairScheduleId);
        if (repairSchedule.isPresent()) {
            RepairSchedule newSchedule = repairSchedule.get().with().nextActivation(DateTime.now())
                    .build(repairScheduleId);

            context.storage.updateRepairSchedule(newSchedule);
            return Response.ok().entity(getRepairScheduleStatus(newSchedule)).build();
        } else {
            return Response.status(404).entity("repair schedule with id " + repairScheduleId + " doesn't exist")
                    .build();
        }
    }

    /**
     * @param clusterName The cluster_name for which the repair schedule belongs to.
     * @return all know repair schedules for a cluster.
     */
    @GET
    @Path("/cluster/{cluster_name}")
    public Response getRepairSchedulesForCluster(@PathParam("cluster_name") String clusterName) {
        LOG.debug("get repair schedules for cluster called with: cluster_name = {}", clusterName);
        Collection<RepairSchedule> repairSchedules = context.storage.getRepairSchedulesForCluster(clusterName);
        Collection<RepairScheduleStatus> repairScheduleViews = new ArrayList<>();
        for (RepairSchedule repairSchedule : repairSchedules) {
            repairScheduleViews.add(getRepairScheduleStatus(repairSchedule));
        }
        return Response.ok().entity(repairScheduleViews).build();
    }

    /**
     * @return RepairSchedule status for viewing
     */
    private RepairScheduleStatus getRepairScheduleStatus(RepairSchedule repairSchedule) {
        RepairUnit repairUnit = context.storage.getRepairUnit(repairSchedule.getRepairUnitId());
        return new RepairScheduleStatus(repairSchedule, repairUnit);
    }

    /**
     * Crafts an URI used to identify given repair schedule.
     *
     * @return The created resource URI.
     */
    private static URI buildRepairScheduleUri(UriInfo uriInfo, RepairSchedule repairSchedule) {
        return uriInfo.getBaseUriBuilder().path("repair_schedule").path(repairSchedule.getId().toString()).build();
    }

    /**
     * @param clusterName The cluster name to list the schedules for. If not given, will list all schedules for all
     *        clusters.
     * @param keyspaceName The keyspace name to list schedules for. Limits the returned list and works whether the cluster
     *        name is given or not.
     * @return All schedules in the system.
     */
    @GET
    public Response listSchedules(@QueryParam("clusterName") Optional<String> clusterName,
            @QueryParam("keyspace") Optional<String> keyspaceName) {

        List<RepairScheduleStatus> scheduleStatuses = Lists.newArrayList();
        getScheduleList(clusterName, keyspaceName).forEach((schedule) -> {
            RepairUnit unit = context.storage.getRepairUnit(schedule.getRepairUnitId());
            scheduleStatuses.add(new RepairScheduleStatus(schedule, unit));
        });
        return Response.ok().entity(scheduleStatuses).build();
    }

    private Collection<RepairSchedule> getScheduleList(Optional<String> clusterName,
            Optional<String> keyspaceName) {
        Collection<RepairSchedule> schedules;
        if (clusterName.isPresent() && keyspaceName.isPresent()) {
            schedules = context.storage.getRepairSchedulesForClusterAndKeyspace(clusterName.get(),
                    keyspaceName.get());
        } else if (clusterName.isPresent()) {
            schedules = context.storage.getRepairSchedulesForCluster(clusterName.get());
        } else if (keyspaceName.isPresent()) {
            schedules = context.storage.getRepairSchedulesForKeyspace(keyspaceName.get());
        } else {
            schedules = context.storage.getAllRepairSchedules();
        }
        return schedules;
    }

    /**
     * Delete a RepairSchedule object with given id.
     *
     * <p>
     * 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 202 response code if the delete has been accepted, 409 if schedule can't be stopped.
     */
    @DELETE
    @Path("/{id}")
    public Response deleteRepairSchedule(@PathParam("id") UUID 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()) {
            if (RepairSchedule.State.ACTIVE == scheduleToDelete.get().getState()) {
                String msg = String.format("Repair schedule %s currently running. Must be first stopped",
                        repairScheduleId);
                return Response.status(Response.Status.CONFLICT).entity(msg).build();
            }
            if (!scheduleToDelete.get().getOwner().equalsIgnoreCase(owner.get())) {
                String msg = String.format("Repair schedule %s is not owned by %s", repairScheduleId, owner.get());
                return Response.status(Response.Status.CONFLICT).entity(msg).build();
            }
            context.storage.deleteRepairSchedule(repairScheduleId);
            return Response.accepted().build();
        }
        return Response.status(Response.Status.NOT_FOUND)
                .entity("Repair schedule with id \"" + repairScheduleId + "\" not found").build();
    }

    private DateTime getNextActivationTime(Optional<String> scheduleTriggerTime) throws IllegalArgumentException {
        DateTime nextActivation;
        if (scheduleTriggerTime.isPresent()) {
            nextActivation = DateTime.parse(scheduleTriggerTime.get());
            LOG.info("first schedule activation will be: {}", RepairRunStatus.dateTimeToIso8601(nextActivation));
        } else {
            nextActivation = DateTime.now().plusDays(1).withTimeAtStartOfDay();
            LOG.info("no schedule_trigger_time given, so setting first scheduling next night: {}",
                    RepairRunStatus.dateTimeToIso8601(nextActivation));
        }
        return nextActivation;
    }
}