Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint to allow users to delete pending on-demand tasks #1653

Merged
merged 8 commits into from Dec 7, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<SingularityPendingTaskId> getPendingTasksMarkedForDeletion() {
if (leaderCache.active()) {
return leaderCache.getPendingTaskIdsToDelete();
}

return getChildrenAsIds(PENDING_TASKS_TO_DELETE_PATH_ROOT, pendingTaskIdTranscoder);
}

public void deleteCleanupTask(String taskId) {
Expand Down
Expand Up @@ -92,6 +92,10 @@ public SingularityMesosOfferScheduler(MesosConfiguration mesosConfiguration,
}

public List<SingularityOfferHolder> checkOffers(final Collection<Offer> offers) {
for (SingularityPendingTaskId taskId : taskManager.getPendingTasksMarkedForDeletion()) {
taskManager.deletePendingTask(taskId);
}

boolean useTaskCredits = disasterManager.isTaskCreditEnabled();
int taskCredits = useTaskCredits ? disasterManager.getUpdatedCreditCount() : -1;

Expand Down
@@ -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;

Expand Down Expand Up @@ -40,6 +41,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;
Expand All @@ -50,6 +52,7 @@
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;
Expand Down Expand Up @@ -180,6 +183,31 @@ public SingularityTaskRequest getPendingTask(@Auth SingularityUser user, @PathPa
return taskRequestList.get(0);
}

@DELETE
@Path("/scheduled/task/{scheduledTaskId}")
@ApiOperation("Delete a scheduled task.")
public Optional<SingularityPendingTask> deleteScheduledTask(@Auth SingularityUser user, @PathParam("scheduledTaskId") String taskId, @Context HttpServletRequest requestContext) {
return maybeProxyToLeader(requestContext, Optional.class, null, () -> deleteScheduledTask(taskId, user));
}

public Optional<SingularityPendingTask> deleteScheduledTask(String taskId, SingularityUser user) {
Optional<SingularityPendingTask> maybePendingTask = taskManager.getPendingTask(getPendingTaskIdFromStr(taskId));

if (maybePendingTask.isPresent()) {
SingularityPendingTaskId pendingTaskId = maybePendingTask.get().getPendingTaskId();

Optional<SingularityRequestWithState> 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}")
Expand Down
Expand Up @@ -45,6 +45,7 @@ public class SingularityLeaderCache {
private Map<SingularityTaskId, Map<ExtendedTaskState, SingularityTaskHistoryUpdate>> historyUpdates;
private Map<String, SingularitySlave> slaves;
private Map<String, SingularityRack> racks;
private Set<SingularityPendingTaskId> pendingTaskIdsToDelete;

private volatile boolean active;

Expand All @@ -62,6 +63,11 @@ public void cachePendingTasks(List<SingularityPendingTask> pendingTasks) {
pendingTasks.forEach((t) -> pendingTaskIdToPendingTask.put(t.getPendingTaskId(), t));
}

public void cachePendingTasksToDelete(List<SingularityPendingTaskId> pendingTaskIds) {
this.pendingTaskIdsToDelete = new HashSet<>(pendingTaskIds.size());
pendingTaskIdsToDelete.addAll(pendingTaskIds);
}

public void cacheActiveTaskIds(List<SingularityTaskId> activeTaskIds) {
this.activeTaskIds = Collections.synchronizedSet(new HashSet<SingularityTaskId>(activeTaskIds.size()));
activeTaskIds.forEach(this.activeTaskIds::add);
Expand Down Expand Up @@ -126,12 +132,20 @@ public List<SingularityPendingTaskId> getPendingTaskIdsForRequest(String request
.collect(Collectors.toList());
}

public List<SingularityPendingTaskId> getPendingTaskIdsToDelete() { return new ArrayList<SingularityPendingTaskId>(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);
}

Expand Down
8 changes: 8 additions & 0 deletions SingularityUI/app/actions/api/tasks.es6
Expand Up @@ -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}`
})
);
@@ -0,0 +1,51 @@
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 = (
<ToolTip id="delete">
Delete this pending task
</ToolTip>
);

export default class DeletePendingTaskButton extends Component {

static propTypes = {
taskId: PropTypes.string.isRequired,
requestType: PropTypes.string.isRequired,
then: PropTypes.func
};

static defaultProps = {
children: (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining the children here seems weird, especially since the component is only being used in one place. Do we need the ability to override the button's contents? Either way, I would move the logic into the render function rather than defaultProps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No override necessary that I can think of. Curious, why is it done this way in the PauseButton?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, this UI has a lot of demons ha. I don't blame you for copying it though. In this case I think it's fine if you just hardcode it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, figured out why it's defined here - the getClickComponent hooks up the onClick logic based on props.children. I'll leave for now unless you have better alternative?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine if it'll be a bigger refactor. Better to have it be consistent for now.

<OverlayTrigger placement="top" id="view-delete-pending-task-overlay" overlay={deletePendingTaskTooltip}>
<a>
<Glyphicon glyph="trash" />
</a>
</OverlayTrigger>
)
};

render() {
if (this.props.requestType == "ON_DEMAND") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than making this check here, I would only render the button in the first place if the type is ON_DEMAND. Then you don't need to pass the requestType as a prop at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the DeletePendingTaskButton is placed inside the ScheduledActions constant, how can I make it dynamic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could wrap it in a conditional, like

{cellData.pendingTask.pendingTaskId.id &&
    <DeletePendingTaskButton 
        taskId={cellData.pendingTask.pendingTaskId.id}
        requestType={cellData.request.requestType}
    />
}

return (
<span>
{getClickComponent(this)}
<DeletePendingTaskModal
ref="modal"
taskId={this.props.taskId}
requestType={this.props.requestType}
then={this.props.then}
/>
</span>
);
}
return (null);
}
}
@@ -0,0 +1,45 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';

import { DeletePendingOnDemandTask } from '../../../actions/api/tasks';

import FormModal from '../../common/modal/FormModal';

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 (
<FormModal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not already have a confirm modal? Feels bad doing it as a FormModal with no fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, we do have one! I'll swap it out.

name="Delete Pending Task"
ref="deletePendingTaskModal"
action="Delete Pending Task"
onConfirm={() => this.props.deletePendingTask()}
buttonStyle="primary"
formElements={[]}>
<p>Are you sure you want to delete this task?</p>
<pre>{this.props.taskId}</pre>
</FormModal>
);
}
}

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);
4 changes: 4 additions & 0 deletions SingularityUI/app/components/tasks/Columns.jsx
Expand Up @@ -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';
Expand Down Expand Up @@ -307,6 +308,9 @@ export const ScheduledActions = (
cellRender={(cellData) => (
<div className="hidden-xs">
<RunNowButton requestId={cellData.pendingTask.pendingTaskId.requestId} />
<DeletePendingTaskButton taskId={cellData.pendingTask.pendingTaskId.id}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: taskId should be on a new line here.

requestType={cellData.request.requestType}
/>
<JSONButton className="inline" object={cellData} showOverlay={true}>
{'{ }'}
</JSONButton>
Expand Down
4 changes: 2 additions & 2 deletions compose-dev.yml
Expand Up @@ -10,8 +10,8 @@ master:
net: host
environment:
MESOS_ZK: zk://localhost:2181/mesos
MESOS_HOSTNAME: localhost
MESOS_IP: 127.0.0.1
MESOS_HOSTNAME: 192.168.99.100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a fix for this in #1655 , I'll merge through to master so you can pull it in

MESOS_IP: 192.168.99.100
MESOS_QUORUM: 1
MESOS_CLUSTER: docker-compose
MESOS_WORK_DIR: /var/lib/mesos
Expand Down