All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hubspot.singularity.resources.RequestResource Maven / Gradle / Ivy

package com.hubspot.singularity.resources;

import java.util.List;
import java.util.Map;

import javax.ws.rs.Consumes;
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.MediaType;

import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.hubspot.jackson.jaxrs.PropertyFiltering;
import com.hubspot.singularity.RequestState;
import com.hubspot.singularity.SingularityCreateResult;
import com.hubspot.singularity.SingularityDeploy;
import com.hubspot.singularity.SingularityPendingDeploy;
import com.hubspot.singularity.SingularityPendingRequest;
import com.hubspot.singularity.SingularityPendingRequest.PendingType;
import com.hubspot.singularity.SingularityRequest;
import com.hubspot.singularity.SingularityRequestCleanup;
import com.hubspot.singularity.SingularityRequestCleanup.RequestCleanupType;
import com.hubspot.singularity.SingularityRequestDeployState;
import com.hubspot.singularity.SingularityRequestHistory.RequestHistoryType;
import com.hubspot.singularity.SingularityRequestInstances;
import com.hubspot.singularity.SingularityRequestParent;
import com.hubspot.singularity.SingularityRequestWithState;
import com.hubspot.singularity.SingularityService;
import com.hubspot.singularity.WebExceptions;
import com.hubspot.singularity.api.SingularityPauseRequest;
import com.hubspot.singularity.data.DeployManager;
import com.hubspot.singularity.data.RequestManager;
import com.hubspot.singularity.data.SingularityValidator;
import com.hubspot.singularity.data.TaskManager;
import com.hubspot.singularity.smtp.SingularityMailer;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import com.wordnik.swagger.annotations.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses;

@Path(RequestResource.PATH)
@Produces({ MediaType.APPLICATION_JSON })
@Api(description="Manages Singularity Requests, the parent object for any deployed task", value=RequestResource.PATH, position=1)
public class RequestResource extends AbstractRequestResource {
  public static final String PATH = SingularityService.API_BASE_PATH + "/requests";

  private final SingularityValidator validator;

  private final SingularityMailer mailer;
  private final TaskManager taskManager;
  private final RequestManager requestManager;
  private final DeployManager deployManager;

  @Inject
  public RequestResource(SingularityValidator validator, DeployManager deployManager, TaskManager taskManager, RequestManager requestManager, SingularityMailer mailer) {
    super(requestManager, deployManager);

    this.validator = validator;
    this.mailer = mailer;
    this.taskManager = taskManager;
    this.deployManager = deployManager;
    this.requestManager = requestManager;
  }

  private static class SingularityRequestDeployHolder {

    private final Optional activeDeploy;
    private final Optional pendingDeploy;

    public SingularityRequestDeployHolder(Optional activeDeploy, Optional pendingDeploy) {
      this.activeDeploy = activeDeploy;
      this.pendingDeploy = pendingDeploy;
    }

    public Optional getActiveDeploy() {
      return activeDeploy;
    }

    public Optional getPendingDeploy() {
      return pendingDeploy;
    }

  }

  private SingularityRequestDeployHolder getDeployHolder(String requestId) {
    Optional requestDeployState = deployManager.getRequestDeployState(requestId);

    Optional activeDeploy = Optional.absent();
    Optional pendingDeploy = Optional.absent();

    if (requestDeployState.isPresent()) {
      if (requestDeployState.get().getActiveDeploy().isPresent()) {
        activeDeploy = deployManager.getDeploy(requestId, requestDeployState.get().getActiveDeploy().get().getDeployId());
      }
      if (requestDeployState.get().getPendingDeploy().isPresent()) {
        pendingDeploy = deployManager.getDeploy(requestId, requestDeployState.get().getPendingDeploy().get().getDeployId());
      }
    }

    return new SingularityRequestDeployHolder(activeDeploy, pendingDeploy);
  }

  @POST
  @Consumes({ MediaType.APPLICATION_JSON })
  @ApiOperation(value="Create or update a Singularity Request", response=SingularityRequestParent.class)
  @ApiResponses({
    @ApiResponse(code=400, message="Request object is invalid"),
    @ApiResponse(code=409, message="Request object is being cleaned. Try again shortly"),
  })
  public SingularityRequestParent submit(@ApiParam("The Singularity request to create or update") SingularityRequest request,
      @ApiParam("Username of the person requesting to create or update") @QueryParam("user") Optional user) {
    if (request.getId() == null) {
      throw WebExceptions.badRequest("Request must have an id");
    }

    Optional maybeOldRequestWithState = requestManager.getRequest(request.getId());
    Optional maybeOldRequest = maybeOldRequestWithState.isPresent() ? Optional.of(maybeOldRequestWithState.get().getRequest()) : Optional. absent();

    SingularityRequestDeployHolder deployHolder = getDeployHolder(request.getId());

    SingularityRequest newRequest = validator.checkSingularityRequest(request, maybeOldRequest, deployHolder.getActiveDeploy(), deployHolder.getPendingDeploy());

    if (!maybeOldRequest.isPresent() && requestManager.cleanupRequestExists(request.getId())) {
      throw WebExceptions.conflict("Request %s is currently cleaning. Try again after a few moments", request.getId());
    }

    final long now = System.currentTimeMillis();

    requestManager.activate(newRequest, maybeOldRequest.isPresent() ? RequestHistoryType.UPDATED : RequestHistoryType.CREATED, now, user);

    checkReschedule(newRequest, maybeOldRequest, now);

    return fillEntireRequest(fetchRequestWithState(request.getId()));
  }

  private void checkReschedule(SingularityRequest newRequest, Optional maybeOldRequest, long timestamp) {
    if (!maybeOldRequest.isPresent()) {
      return;
    }

    if (shouldReschedule(newRequest, maybeOldRequest.get())) {
      Optional maybeDeployId = deployManager.getInUseDeployId(newRequest.getId());

      if (maybeDeployId.isPresent()) {
        requestManager.addToPendingQueue(new SingularityPendingRequest(newRequest.getId(), maybeDeployId.get(), timestamp, PendingType.UPDATED_REQUEST));
      }
    }
  }

  private boolean shouldReschedule(SingularityRequest newRequest, SingularityRequest oldRequest) {
    if (newRequest.getInstancesSafe() != oldRequest.getInstancesSafe()) {
      return true;
    }
    if (newRequest.isScheduled() && oldRequest.isScheduled()) {
      if (!newRequest.getQuartzScheduleSafe().equals(oldRequest.getQuartzScheduleSafe())) {
        return true;
      }
    }

    return false;
  }

  private String getAndCheckDeployId(String requestId) {
    Optional maybeDeployId = deployManager.getInUseDeployId(requestId);

    if (!maybeDeployId.isPresent()) {
      throw WebExceptions.conflict("Can not schedule/bounce a request (%s) with no deploy", requestId);
    }

    return maybeDeployId.get();
  }

  @POST
  @Path("/request/{requestId}/bounce")
  @ApiOperation(value="Bounce a specific Singularity request. A bounce launches replacement task(s), and then kills the original task(s) if the replacement(s) are healthy.",
  response=SingularityRequestParent.class)
  public SingularityRequestParent bounce(@ApiParam("The request ID to bounce") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the bounce") @QueryParam("user") Optional user) {
    SingularityRequestWithState requestWithState = fetchRequestWithState(requestId);

    if (!requestWithState.getRequest().isLongRunning()) {
      throw WebExceptions.badRequest("Can not bounce a scheduled or one-off request (%s)", requestWithState);
    }

    checkRequestStateNotPaused(requestWithState, "bounce");

    SingularityCreateResult createResult = requestManager.createCleanupRequest(
        new SingularityRequestCleanup(user, RequestCleanupType.BOUNCE, System.currentTimeMillis(), Optional. absent(), requestId, Optional.of(getAndCheckDeployId(requestId))));

    if (createResult == SingularityCreateResult.EXISTED) {
      throw WebExceptions.conflict("%s is already bouncing", requestId);
    }

    return fillEntireRequest(requestWithState);
  }

  @POST
  @Path("/request/{requestId}/run")
  @ApiOperation(value="Schedule a one-off or scheduled Singularity request for immediate execution.", response=SingularityRequestParent.class)
  @ApiResponses({
    @ApiResponse(code=400, message="Singularity Request is not scheduled or one-off"),
  })
  public SingularityRequestParent scheduleImmediately(@ApiParam("The request ID to run") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the execution") @QueryParam("user") Optional user,
      @ApiParam("Additional command line arguments to append to the task") String commandLineArgs) {
    SingularityRequestWithState requestWithState = fetchRequestWithState(requestId);

    checkRequestStateNotPaused(requestWithState, "run now");

    Optional maybeCmdLineArgs = Optional.absent();

    PendingType pendingType = null;

    if (requestWithState.getRequest().isScheduled()) {
      pendingType = PendingType.IMMEDIATE;

      if (!taskManager.getActiveTaskIdsForRequest(requestId).isEmpty()) {
        throw WebExceptions.conflict("Can not request an immediate run of a scheduled job which is currently running (%s)", taskManager.getActiveTaskIdsForRequest(requestId));
      }
    } else if (requestWithState.getRequest().isOneOff()) {
      pendingType = PendingType.ONEOFF;
    } else {
      throw WebExceptions.badRequest("Can not request an immediate run of a non-scheduled / always running request (%s)", requestWithState.getRequest());
    }

    if (!Strings.isNullOrEmpty(commandLineArgs)) {
      maybeCmdLineArgs = Optional.of(commandLineArgs);
    }

    SingularityCreateResult result = requestManager.addToPendingQueue(new SingularityPendingRequest(requestId, getAndCheckDeployId(requestId), System.currentTimeMillis(), maybeCmdLineArgs, user, pendingType));

    if (result == SingularityCreateResult.EXISTED) {
      throw WebExceptions.conflict("%s is already pending, please try again soon", requestId);
    }

    return fillEntireRequest(requestWithState);
  }

  @POST
  @Path("/request/{requestId}/pause")
  @ApiOperation(value="Pause a Singularity request, future tasks will not run until it is manually unpaused. API can optionally choose to kill existing tasks", response=SingularityRequestParent.class)
  @ApiResponses({
    @ApiResponse(code=409, message="Request is already paused or being cleaned"),
  })
  public SingularityRequestParent pause(@ApiParam("The request ID to pause") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the pause") @QueryParam("user") Optional user,
      @ApiParam("Pause Request Options") Optional pauseRequest) {
    SingularityRequestWithState requestWithState = fetchRequestWithState(requestId);

    checkRequestStateNotPaused(requestWithState, "pause");

    Optional killTasks = Optional.absent();
    if (pauseRequest.isPresent()) {
      user = pauseRequest.get().getUser();
      killTasks = pauseRequest.get().getKillTasks();
    }

    final long now = System.currentTimeMillis();

    SingularityCreateResult result = requestManager.createCleanupRequest(new SingularityRequestCleanup(user, RequestCleanupType.PAUSING, now, killTasks, requestId, Optional. absent()));

    if (result == SingularityCreateResult.EXISTED) {
      throw WebExceptions.conflict("%s is already pausing - try again soon", requestId, result);
    }

    mailer.sendRequestPausedMail(requestWithState.getRequest(), user);

    requestManager.pause(requestWithState.getRequest(), now, user);

    return fillEntireRequest(new SingularityRequestWithState(requestWithState.getRequest(), RequestState.PAUSED, now));
  }

  @POST
  @Path("/request/{requestId}/unpause")
  @ApiOperation(value="Unpause a Singularity Request, scheduling new tasks immediately", response=SingularityRequestParent.class)
  @ApiResponses({
    @ApiResponse(code=409, message="Request is not paused"),
  })
  public SingularityRequestParent unpause(@ApiParam("The request ID to unpause") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the unpause") @QueryParam("user") Optional user) {
    SingularityRequestWithState requestWithState = fetchRequestWithState(requestId);

    if (requestWithState.getState() != RequestState.PAUSED) {
      throw WebExceptions.conflict("Request %s is not in PAUSED state, it is in %s", requestId, requestWithState.getState());
    }

    mailer.sendRequestUnpausedMail(requestWithState.getRequest(), user);

    Optional maybeDeployId = deployManager.getInUseDeployId(requestId);

    final long now = System.currentTimeMillis();

    requestManager.unpause(requestWithState.getRequest(), now, user);

    if (maybeDeployId.isPresent() && !requestWithState.getRequest().isOneOff()) {
      requestManager.addToPendingQueue(new SingularityPendingRequest(requestId, maybeDeployId.get(), now, Optional. absent(), user, PendingType.UNPAUSED));
    }

    return fillEntireRequest(new SingularityRequestWithState(requestWithState.getRequest(), RequestState.ACTIVE, now));
  }

  @GET
  @PropertyFiltering
  @Path("/active")
  @ApiOperation(value="Retrieve the list of active requests", response=SingularityRequestParent.class, responseContainer="List")
  public List getActiveRequests() {
    return getRequestsWithDeployState(requestManager.getActiveRequests());
  }

  private List getRequestsWithDeployState(Iterable requests) {
    List requestIds = Lists.newArrayList();
    for (SingularityRequestWithState requestWithState : requests) {
      requestIds.add(requestWithState.getRequest().getId());
    }

    List parents = Lists.newArrayListWithCapacity(requestIds.size());

    Map deployStates = deployManager.getRequestDeployStatesByRequestIds(requestIds);

    for (SingularityRequestWithState requestWithState : requests) {
      Optional deployState = Optional.fromNullable(deployStates.get(requestWithState.getRequest().getId()));
      parents.add(new SingularityRequestParent(requestWithState.getRequest(), requestWithState.getState(), deployState, Optional. absent(), Optional. absent(), Optional. absent()));
    }

    return parents;
  }

  @GET
  @PropertyFiltering
  @Path("/paused")
  @ApiOperation(value="Retrieve the list of paused requests", response=SingularityRequestParent.class, responseContainer="List")
  public List getPausedRequests() {
    return getRequestsWithDeployState(requestManager.getPausedRequests());
  }

  @GET
  @PropertyFiltering
  @Path("/cooldown")
  @ApiOperation(value="Retrieve the list of requests in system cooldown", response=SingularityRequestParent.class, responseContainer="List")
  public List getCooldownRequests() {
    return getRequestsWithDeployState(requestManager.getCooldownRequests());
  }

  @GET
  @PropertyFiltering
  @Path("/finished")
  @ApiOperation(value="Retreive the list of finished requests (Scheduled requests which have exhausted their schedules)", response=SingularityRequestParent.class, responseContainer="List")
  public List getFinishedRequests() {
    return getRequestsWithDeployState(requestManager.getFinishedRequests());
  }

  @GET
  @PropertyFiltering
  @ApiOperation(value="Retrieve the list of all requests", response=SingularityRequestParent.class, responseContainer="List")
  public List getRequests() {
    return getRequestsWithDeployState(requestManager.getRequests());
  }

  @GET
  @PropertyFiltering
  @Path("/queued/pending")
  @ApiOperation(value="Retrieve the list of pending requests", response=SingularityPendingRequest.class, responseContainer="List")
  public List getPendingRequests() {
    return requestManager.getPendingRequests();
  }

  @GET
  @PropertyFiltering
  @Path("/queued/cleanup")
  @ApiOperation(value="Retrieve the list of requests being cleaned up", response=SingularityRequestCleanup.class, responseContainer="List")
  public List getCleanupRequests() {
    return requestManager.getCleanupRequests();
  }

  @GET
  @Path("/request/{requestId}")
  @ApiOperation(value="Retrieve a specific Request by ID", response=SingularityRequestParent.class)
  @ApiResponses({
    @ApiResponse(code=404, message="No Request with that ID"),
  })
  public SingularityRequestParent getRequest(@ApiParam("Request ID") @PathParam("requestId") String requestId) {
    return fillEntireRequest(fetchRequestWithState(requestId));
  }

  private SingularityRequest fetchRequest(String requestId) {
    return fetchRequestWithState(requestId).getRequest();
  }

  @DELETE
  @Path("/request/{requestId}")
  @ApiOperation(value="Delete a specific Request by ID and return the deleted Request", response=SingularityRequest.class)
  @ApiResponses({
    @ApiResponse(code=404, message="No Request with that ID"),
  })
  public SingularityRequest deleteRequest(@ApiParam("The request ID to delete.") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the delete") @QueryParam("user") Optional user) {
    SingularityRequest request = fetchRequest(requestId);

    requestManager.deleteRequest(request, user);

    mailer.sendRequestRemovedMail(request, user);

    return request;
  }

  @PUT
  @Path("/request/{requestId}/instances")
  @ApiOperation(value="Scale the number of instances up or down for a specific Request", response=SingularityRequest.class)
  @ApiResponses({
    @ApiResponse(code=400, message="Posted object did not match Request ID"),
    @ApiResponse(code=404, message="No Request with that ID"),
  })
  public SingularityRequest updateInstances(@ApiParam("The Request ID to scale") @PathParam("requestId") String requestId,
      @ApiParam("Username of the person requesting the scale") @QueryParam("user") Optional user,
      @ApiParam("Object to hold number of instances to request") SingularityRequestInstances newInstances) {
    if (requestId == null || newInstances.getId() == null || !requestId.equals(newInstances.getId())) {
      throw WebExceptions.badRequest("Update for request instance must pass a matching non-null requestId in path (%s) and object (%s)", requestId, newInstances.getId());
    }

    SingularityRequest oldRequest = fetchRequest(requestId);
    Optional maybeOldRequest = Optional.of(oldRequest);

    SingularityRequestDeployHolder deployHolder = getDeployHolder(newInstances.getId());
    SingularityRequest newRequest = oldRequest.toBuilder().setInstances(newInstances.getInstances()).build();

    validator.checkSingularityRequest(newRequest, maybeOldRequest, deployHolder.getActiveDeploy(), deployHolder.getPendingDeploy());

    final long now = System.currentTimeMillis();

    requestManager.update(newRequest, now, user);

    checkReschedule(newRequest, maybeOldRequest, now);

    return newRequest;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy