Skip to content
This repository has been archived by the owner on Dec 15, 2021. It is now read-only.

Commit

Permalink
Merge pull request #141 from jglick/kill-JENKINS-25550
Browse files Browse the repository at this point in the history
[JENKINS-25550] Hard kill
  • Loading branch information
jglick committed Oct 26, 2015
2 parents 7eb7a8a + d44f5d4 commit 5aca627
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -4,6 +4,7 @@ Only noting significant user changes, not internal code cleanups and minor bug f

## 1.11 (upcoming)

* [JENKINS-25550](https://issues.jenkins-ci.org/browse/JENKINS-25550): flow builds hung due to a buggy step (and certain erroneous scripts) can now be forcibly stopped by using hyperlinks that appear in the console after an initial abort attempt.
* [JENKINS-30974](https://issues.jenkins-ci.org/browse/JENKINS-30974): error during build queue rendering on 1.624+ when using non-concurrent-capable Workflow builds.
* Added the `absoluteUrl` property to `RunWrapper`
* [JENKINS-29542](https://issues.jenkins-ci.org/browse/JENKINS-29542): fixed help display for `env` global variable.
Expand Down
@@ -0,0 +1,118 @@
/*
* The MIT License
*
* Copyright 2015 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.workflow;

import com.google.inject.Inject;
import hudson.model.Executor;
import hudson.model.Result;
import hudson.model.TaskListener;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.RestartableJenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

public class WorkflowRunRestartTest {

@ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
@Rule public RestartableJenkinsRule story = new RestartableJenkinsRule();

@Issue("JENKINS-25550")
@Test public void hardKill() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("def seq = 0; retry (99) {zombie id: ++seq}"));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
story.j.waitForMessage("[1] undead", b);
Executor ex = b.getExecutor();
assertNotNull(ex);
ex.interrupt();
story.j.waitForMessage("[1] bwahaha FlowInterruptedException #1", b);
ex.interrupt();
story.j.waitForMessage("[1] bwahaha FlowInterruptedException #2", b);
b.doTerm();
story.j.waitForMessage("[2] undead", b);
ex.interrupt();
story.j.waitForMessage("[2] bwahaha FlowInterruptedException #1", b);
b.doKill();
story.j.waitForMessage("Hard kill!", b);
story.j.waitForCompletion(b);
story.j.assertBuildStatus(Result.ABORTED, b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class);
WorkflowRun b = p.getBuildByNumber(1);
assertFalse(b.isBuilding());
}
});
}
public static class Zombie extends AbstractStepImpl {
@DataBoundSetter public int id;
@DataBoundConstructor public Zombie() {}
public static class Execution extends AbstractStepExecutionImpl {
@Inject(optional=true) private transient Zombie step;
@StepContextParameter private transient TaskListener listener;
int id;
int count;
@Override public boolean start() throws Exception {
id = step.id;
listener.getLogger().printf("[%d] undead%n", id);
return false;
}
@Override public void stop(Throwable cause) throws Exception {
listener.getLogger().printf("[%d] bwahaha %s #%d%n", id, cause.getClass().getSimpleName(), ++count);
}

}
@TestExtension("hardKill") public static class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() {
super(Execution.class);
}
@Override public String getFunctionName() {
return "zombie";
}
@Override public String getDisplayName() {
return "zombie";
}
}
}

}
Expand Up @@ -17,7 +17,6 @@

import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.nodes.StepNode;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
Expand Down Expand Up @@ -104,12 +103,12 @@ public void interruptedTest() throws Exception {

// At this point syncnonblocking is waiting for an interruption

FlowExecution e = b.getExecutionPromise().get();
// Let's force a call to stop. This will try to send an interruption to the run Thread
e.interrupt(Result.ABORTED);
b.getExecutor().interrupt();
System.out.println("Looking for interruption received log message");
j.waitForMessage("Interrupted!", b);
j.waitForCompletion(b);
j.assertBuildStatus(Result.ABORTED, b);
}

@Test
Expand Down
Expand Up @@ -11,7 +11,6 @@
import java.io.Serializable;
import java.util.concurrent.TimeoutException;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
Expand All @@ -24,13 +23,19 @@
import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable.Row;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;

public class ShellStepTest extends Assert {

@ClassRule
public static BuildWatcher buildWatcher = new BuildWatcher();

@Rule
public JenkinsRule j = new JenkinsRule();

Expand Down Expand Up @@ -81,8 +86,6 @@ public void abort() throws Exception {

// get the build going, and wait until workflow pauses
WorkflowRun b = foo.scheduleBuild2(0).getStartCondition().get();
CpsFlowExecution e = (CpsFlowExecution) b.getExecutionPromise().get();
e.waitForSuspension();

// at this point the file should be being touched
waitForCond(5000, tmp, new Predicate<File>() {
Expand All @@ -92,7 +95,7 @@ public boolean apply(File tmp) {
}
});

e.interrupt(Result.ABORTED);
b.getExecutor().interrupt();

// touching should have stopped
final long refTimestamp = tmp.lastModified();
Expand All @@ -102,6 +105,11 @@ public boolean apply(File tmp) {
return refTimestamp==tmp.lastModified();
}
});

j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(b));
/* TODO fails, even though WorkflowRun calls recordCauseOfInterruption:
assertNotNull(b.getAction(InterruptedBuildAction.class)); // would also have a UserInterruption if authenticated
*/
}

@Test public void launcherDecorator() throws Exception {
Expand Down
Expand Up @@ -24,6 +24,7 @@

package org.jenkinsci.plugins.workflow.actions;

import groovy.lang.MissingMethodException;
import hudson.remoting.ProxyException;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.jenkinsci.plugins.workflow.graph.AtomNode;
Expand Down Expand Up @@ -52,7 +53,8 @@ public ErrorAction(Throwable error) {
* an equivalent that captures the same details but serializes nicely.
*/
private boolean isUnserializableException(Throwable error) {
return error instanceof MultipleCompilationErrorsException;
return error instanceof MultipleCompilationErrorsException ||
error instanceof MissingMethodException;
}

public Throwable getError() {
Expand Down
Expand Up @@ -81,7 +81,6 @@ public static final class Execution extends AbstractStepExecutionImpl implements
@StepContextParameter private transient Launcher launcher;
@StepContextParameter private transient TaskListener listener;
private transient long recurrencePeriod;
private transient int stopAttempt;
private Controller controller;
private String node;
private String remote;
Expand Down Expand Up @@ -136,15 +135,9 @@ public static final class Execution extends AbstractStepExecutionImpl implements
}

@Override public void stop(Throwable cause) throws Exception {
try {
FilePath workspace = getWorkspace();
if (workspace != null) {
controller.stop(workspace, getContext().get(Launcher.class));
}
} finally {
if (stopAttempt++ == 1) { // second attempt
getContext().onFailure(cause);
}
FilePath workspace = getWorkspace();
if (workspace != null) {
controller.stop(workspace, getContext().get(Launcher.class));
}
}

Expand All @@ -153,7 +146,7 @@ public static final class Execution extends AbstractStepExecutionImpl implements
try {
check();
} finally {
if (recurrencePeriod > 0 && stopAttempt < 2) {
if (recurrencePeriod > 0) {
Timer.get().schedule(this, recurrencePeriod, TimeUnit.MILLISECONDS);
}
}
Expand Down

0 comments on commit 5aca627

Please sign in to comment.