diff --git a/SingularityService/src/main/java/com/hubspot/singularity/data/TaskManager.java b/SingularityService/src/main/java/com/hubspot/singularity/data/TaskManager.java index acc0a55449..9b42dbd8f0 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/data/TaskManager.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/data/TaskManager.java @@ -77,6 +77,7 @@ public class TaskManager extends CuratorAsyncManager { private static final String DRIVER_KILLED_PATH_ROOT = TASKS_ROOT + "/killed"; private static final String FINISHED_TASK_MAIL_QUEUE = TASKS_ROOT + "/mailqueue"; private static final String SHELL_REQUESTS_QUEUE_PATH_ROOT = TASKS_ROOT + "/shellqueue"; + private static final String PENDING_TASKS_TO_DELETE_PATH_ROOT = TASKS_ROOT + "/pendingdeletes"; private static final String HISTORY_PATH_ROOT = TASKS_ROOT + "/history"; @@ -259,6 +260,8 @@ private String getPendingPath(SingularityPendingTaskId pendingTaskId) { return ZKPaths.makePath(PENDING_PATH_ROOT, pendingTaskId.getId()); } + private String getPendingTasksToDeletePath(SingularityPendingTaskId pendingTaskId) { return ZKPaths.makePath(PENDING_TASKS_TO_DELETE_PATH_ROOT, pendingTaskId.getId()); } + private String getCleanupPath(String taskId) { return ZKPaths.makePath(CLEANUP_PATH_ROOT, taskId); } @@ -805,6 +808,7 @@ public boolean taskExistsInZk(SingularityTaskId taskId) { public void activateLeaderCache() { leaderCache.cachePendingTasks(fetchPendingTasks()); + leaderCache.cachePendingTasksToDelete(getPendingTasksMarkedForDeletion()); leaderCache.cacheActiveTaskIds(getTaskIds(ACTIVE_PATH_ROOT)); leaderCache.cacheCleanupTasks(fetchCleanupTasks()); leaderCache.cacheKilledTasks(fetchKilledTaskIdRecords()); @@ -1066,6 +1070,20 @@ public void deleteActiveTask(String taskId) { public void deletePendingTask(SingularityPendingTaskId pendingTaskId) { leaderCache.deletePendingTask(pendingTaskId); delete(getPendingPath(pendingTaskId)); + delete(getPendingTasksToDeletePath(pendingTaskId)); + } + + public void markPendingTaskForDeletion(SingularityPendingTaskId pendingTaskId) { + leaderCache.markPendingTaskForDeletion(pendingTaskId); + create(getPendingTasksToDeletePath(pendingTaskId)); + } + + public List getPendingTasksMarkedForDeletion() { + if (leaderCache.active()) { + return leaderCache.getPendingTaskIdsToDelete(); + } + + return getChildrenAsIds(PENDING_TASKS_TO_DELETE_PATH_ROOT, pendingTaskIdTranscoder); } public void deleteCleanupTask(String taskId) { diff --git a/SingularityService/src/main/java/com/hubspot/singularity/mesos/SingularityMesosOfferScheduler.java b/SingularityService/src/main/java/com/hubspot/singularity/mesos/SingularityMesosOfferScheduler.java index 8d4d6b5a84..d4c1c850b2 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/mesos/SingularityMesosOfferScheduler.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/mesos/SingularityMesosOfferScheduler.java @@ -93,6 +93,10 @@ public SingularityMesosOfferScheduler(MesosConfiguration mesosConfiguration, } public List checkOffers(final Collection offers) { + for (SingularityPendingTaskId taskId : taskManager.getPendingTasksMarkedForDeletion()) { + taskManager.deletePendingTask(taskId); + } + boolean useTaskCredits = disasterManager.isTaskCreditEnabled(); int taskCredits = useTaskCredits ? disasterManager.getUpdatedCreditCount() : -1; diff --git a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java index a7501f7081..b78d70a50a 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java @@ -1,6 +1,7 @@ package com.hubspot.singularity.resources; import static com.hubspot.singularity.WebExceptions.badRequest; +import static com.hubspot.singularity.WebExceptions.checkBadRequest; import static com.hubspot.singularity.WebExceptions.checkNotFound; import static com.hubspot.singularity.WebExceptions.notFound; @@ -37,6 +38,7 @@ import com.hubspot.mesos.json.MesosTaskMonitorObject; import com.hubspot.mesos.json.MesosTaskStatisticsObject; import com.hubspot.singularity.InvalidSingularityTaskIdException; +import com.hubspot.singularity.RequestType; import com.hubspot.singularity.SingularityAction; import com.hubspot.singularity.SingularityAuthorizationScope; import com.hubspot.singularity.SingularityCreateResult; @@ -45,6 +47,8 @@ import com.hubspot.singularity.SingularityPendingRequest.PendingType; import com.hubspot.singularity.SingularityPendingTask; import com.hubspot.singularity.SingularityPendingTaskId; +import com.hubspot.singularity.SingularityRequest; +import com.hubspot.singularity.SingularityRequestWithState; import com.hubspot.singularity.SingularityShellCommand; import com.hubspot.singularity.SingularitySlave; import com.hubspot.singularity.SingularityTask; @@ -171,6 +175,31 @@ public SingularityTaskRequest getPendingTask(@Auth SingularityUser user, @PathPa return taskRequestList.get(0); } + @DELETE + @Path("/scheduled/task/{scheduledTaskId}") + @ApiOperation("Delete a scheduled task.") + public Optional deleteScheduledTask(@Auth SingularityUser user, @PathParam("scheduledTaskId") String taskId, @Context HttpServletRequest requestContext) { + return maybeProxyToLeader(requestContext, Optional.class, null, () -> deleteScheduledTask(taskId, user)); + } + + public Optional deleteScheduledTask(String taskId, SingularityUser user) { + Optional maybePendingTask = taskManager.getPendingTask(getPendingTaskIdFromStr(taskId)); + + if (maybePendingTask.isPresent()) { + SingularityPendingTaskId pendingTaskId = maybePendingTask.get().getPendingTaskId(); + + Optional maybeRequest = requestManager.getRequest(pendingTaskId.getRequestId()); + checkNotFound(maybeRequest.isPresent(), "Couldn't find: " + taskId); + + SingularityRequest request = maybeRequest.get().getRequest(); + authorizationHelper.checkForAuthorizationByRequestId(request.getId(), user, SingularityAuthorizationScope.WRITE); + checkBadRequest(request.getRequestType() == RequestType.ON_DEMAND, "Only ON_DEMAND tasks may be deleted."); + + taskManager.markPendingTaskForDeletion(pendingTaskId); + } + return maybePendingTask; + } + @GET @PropertyFiltering @Path("/scheduled/request/{requestId}") diff --git a/SingularityService/src/main/java/com/hubspot/singularity/scheduler/SingularityLeaderCache.java b/SingularityService/src/main/java/com/hubspot/singularity/scheduler/SingularityLeaderCache.java index c186596529..2e8d4e1177 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/scheduler/SingularityLeaderCache.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/scheduler/SingularityLeaderCache.java @@ -45,6 +45,7 @@ public class SingularityLeaderCache { private Map> historyUpdates; private Map slaves; private Map racks; + private Set pendingTaskIdsToDelete; private volatile boolean active; @@ -62,6 +63,11 @@ public void cachePendingTasks(List pendingTasks) { pendingTasks.forEach((t) -> pendingTaskIdToPendingTask.put(t.getPendingTaskId(), t)); } + public void cachePendingTasksToDelete(List pendingTaskIds) { + this.pendingTaskIdsToDelete = new HashSet<>(pendingTaskIds.size()); + pendingTaskIdsToDelete.addAll(pendingTaskIds); + } + public void cacheActiveTaskIds(List activeTaskIds) { this.activeTaskIds = Collections.synchronizedSet(new HashSet(activeTaskIds.size())); activeTaskIds.forEach(this.activeTaskIds::add); @@ -126,12 +132,20 @@ public List getPendingTaskIdsForRequest(String request .collect(Collectors.toList()); } + public List getPendingTaskIdsToDelete() { return new ArrayList(pendingTaskIdsToDelete); } + + public void markPendingTaskForDeletion(SingularityPendingTaskId taskId) { + pendingTaskIdsToDelete.add(taskId); + } + public void deletePendingTask(SingularityPendingTaskId pendingTaskId) { if (!active) { LOG.warn("deletePendingTask {}, but not active", pendingTaskId); return; } - + if (pendingTaskIdsToDelete.contains(pendingTaskId)) { + pendingTaskIdsToDelete.remove(pendingTaskId); + } pendingTaskIdToPendingTask.remove(pendingTaskId); } diff --git a/SingularityUI/app/actions/api/tasks.es6 b/SingularityUI/app/actions/api/tasks.es6 index 2997969fb0..0017ee1bbe 100644 --- a/SingularityUI/app/actions/api/tasks.es6 +++ b/SingularityUI/app/actions/api/tasks.es6 @@ -71,3 +71,11 @@ export const RunCommandOnTask = buildJsonApiAction( body: {name: commandName} }) ); + +export const DeletePendingOnDemandTask = buildJsonApiAction( + 'DELETE_PENDING_ON_DEMAND_TASK', + 'DELETE', + (taskId) => ({ + url: `/tasks/scheduled/task/${taskId}` + }) +); diff --git a/SingularityUI/app/components/common/modalButtons/DeletePendingTaskButton.jsx b/SingularityUI/app/components/common/modalButtons/DeletePendingTaskButton.jsx new file mode 100644 index 0000000000..88d810cbcd --- /dev/null +++ b/SingularityUI/app/components/common/modalButtons/DeletePendingTaskButton.jsx @@ -0,0 +1,48 @@ +import React, { Component, PropTypes } from 'react'; + +import { Glyphicon } from 'react-bootstrap'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import ToolTip from 'react-bootstrap/lib/Tooltip'; + +import { getClickComponent } from '../modal/ModalWrapper'; + +import DeletePendingTaskModal from './DeletePendingTaskModal'; + +const deletePendingTaskTooltip = ( + + Delete this pending task + +); + +export default class DeletePendingTaskButton extends Component { + + static propTypes = { + taskId: PropTypes.string.isRequired, + requestType: PropTypes.string.isRequired, + then: PropTypes.func + }; + + static defaultProps = { + children: ( + + + + + + ) + }; + + render() { + return ( + + {getClickComponent(this)} + + + ); + } +} diff --git a/SingularityUI/app/components/common/modalButtons/DeletePendingTaskModal.jsx b/SingularityUI/app/components/common/modalButtons/DeletePendingTaskModal.jsx new file mode 100644 index 0000000000..00429141ed --- /dev/null +++ b/SingularityUI/app/components/common/modalButtons/DeletePendingTaskModal.jsx @@ -0,0 +1,44 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +import { DeletePendingOnDemandTask } from '../../../actions/api/tasks'; + +import ConfirmationDialog from '../../common/ConfirmationDialog'; + +class DeletePendingTaskModal extends Component { + static propTypes = { + taskId: PropTypes.string.isRequired, + requestType: PropTypes.string.isRequired, + deletePendingTask: PropTypes.func.isRequired, + then: PropTypes.func + }; + + show() { + this.refs.deletePendingTaskModal.show(); + } + + render() { + return ( + this.props.deletePendingTask()} + buttonStyle="primary"> +

Are you sure you want to delete this task?

+
{this.props.taskId}
+
+ ); + } +} + +const mapDispatchToProps = (dispatch, ownProps) => ({ + deletePendingTask: () => dispatch(DeletePendingOnDemandTask.trigger(ownProps.taskId)).then(response => (ownProps.then && ownProps.then(response))) +}); + +export default connect( + null, + mapDispatchToProps, + null, + { withRef: true } +)(DeletePendingTaskModal); diff --git a/SingularityUI/app/components/tasks/Columns.jsx b/SingularityUI/app/components/tasks/Columns.jsx index 19bb66bd1d..e05ad41f8f 100644 --- a/SingularityUI/app/components/tasks/Columns.jsx +++ b/SingularityUI/app/components/tasks/Columns.jsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import Utils from '../../utils'; +import DeletePendingTaskButton from '../common/modalButtons/DeletePendingTaskButton'; import JSONButton from '../common/JSONButton'; import KillTaskButton from '../common/modalButtons/KillTaskButton'; import RunNowButton from '../common/modalButtons/RunNowButton'; @@ -307,6 +308,12 @@ export const ScheduledActions = ( cellRender={(cellData) => (
+ {cellData.request.requestType == "ON_DEMAND" && + + } {'{ }'}