Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #46 from jenkinsci/JENKINS-25623
[JENKINS-25623] time out & kill infinite recursion
  • Loading branch information
jglick committed Sep 7, 2016
2 parents 1100650 + 2196cf6 commit f4a6607
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 189 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -166,7 +166,7 @@
<dependency>
<groupId>com.cloudbees</groupId>
<artifactId>groovy-cps</artifactId>
<version>1.9</version>
<version>1.10-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.ui</groupId>
Expand Down
Expand Up @@ -191,18 +191,7 @@ public boolean cancel(final CauseOfInterruption... causes) {
t.getExecution().runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
@Override
public void onSuccess(CpsThreadGroup g) {
StepExecution s = t.getStep(); // this is the part that should run in CpsVmThread
if (s == null) {
// TODO: if it's not running inside a StepExecution, we need to set an interrupt flag
// and interrupt at an earliest convenience
return;
}

try {
s.stop(stopped);
} catch (Exception e) {
LOGGER.log(WARNING, "Failed to stop " + s, e);
}
t.stop(stopped);
}

@Override
Expand Down
Expand Up @@ -826,13 +826,18 @@ public void interrupt(Result result, CauseOfInterruption... causes) throws IOExc
final FlowInterruptedException ex = new FlowInterruptedException(result,causes);

// stop all ongoing activities
Futures.addCallback(getCurrentExecutions(/* cf. JENKINS-26148 */true), new FutureCallback<List<StepExecution>>() {
runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
@Override
public void onSuccess(List<StepExecution> l) {
LOGGER.log(Level.FINE, "Interrupt of {0} processed on {1}", new Object[] {owner, l});
for (StepExecution e : Iterators.reverse(l)) {
public void onSuccess(CpsThreadGroup g) {
// don't touch outer ones. See JENKINS-26148
Map<FlowHead, CpsThread> m = new LinkedHashMap<>();
for (CpsThread t : g.threads.values()) {
m.put(t.head, t);
}
// for each inner most CpsThread, from young to old...
for (CpsThread t : Iterators.reverse(ImmutableList.copyOf(m.values()))) {
try {
e.stop(ex);
t.stop(ex);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "Failed to abort " + owner, x);
}
Expand Down
Expand Up @@ -77,7 +77,9 @@ public CpsGroovyShellFactory withParent(ClassLoader parent) {

private CpsTransformer makeCpsTransformer() {
CpsTransformer t = sandbox ? new SandboxCpsTransformer() : new CpsTransformer();
t.setConfiguration(new TransformerConfiguration().withClosureType(CpsClosure2.class));
t.setConfiguration(new TransformerConfiguration()
.withClosureType(CpsClosure2.class)
.withSafepoint(Safepoint.class,"safepoint"));
return t;
}

Expand Down
123 changes: 76 additions & 47 deletions src/main/java/org/jenkinsci/plugins/workflow/cps/CpsThread.java
Expand Up @@ -29,6 +29,7 @@
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.SettableFuture;
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepExecution;

import javax.annotation.CheckForNull;
Expand Down Expand Up @@ -151,62 +152,59 @@ public StepExecution getStep() {
@Nonnull Outcome runNextChunk() throws IOException {
assert program!=null;

while (true) {
Outcome outcome;

final CpsThread old = CURRENT.get();
CURRENT.set(this);

try {
LOGGER.log(FINE, "runNextChunk on {0}", resumeValue);
Outcome o = resumeValue;
resumeValue = null;
outcome = program.run0(o);
if (outcome.getAbnormal() != null) {
LOGGER.log(FINE, "ran and produced error", outcome.getAbnormal());
} else {
LOGGER.log(FINE, "ran and produced {0}", outcome);
}
} finally {
CURRENT.set(old);
Outcome outcome;

final CpsThread old = CURRENT.get();
CURRENT.set(this);

try {
LOGGER.log(FINE, "runNextChunk on {0}", resumeValue);
Outcome o = resumeValue;
resumeValue = null;
outcome = program.run0(o);
if (outcome.getAbnormal() != null) {
LOGGER.log(FINE, "ran and produced error", outcome.getAbnormal());
} else {
LOGGER.log(FINE, "ran and produced {0}", outcome);
}
} finally {
CURRENT.set(old);
}

if (outcome.getNormal() instanceof ThreadTask) {
// if an execution in the thread safepoint is requested, deliver that
ThreadTask sc = (ThreadTask) outcome.getNormal();
ThreadTaskResult r = sc.eval(this);
if (r.resume!=null) {
// keep evaluating the CPS code
resumeValue = r.resume;
continue;
} else {
// break but with a different value
outcome = r.suspend;
}
if (outcome.getNormal() instanceof ThreadTask) {
// if an execution in the thread safepoint is requested, deliver that
ThreadTask sc = (ThreadTask) outcome.getNormal();
ThreadTaskResult r = sc.eval(this);
if (r.resume!=null) {
// yield, then keep evaluating the CPS code
resumeValue = r.resume;
} else {
// break but with a different value
outcome = r.suspend;
}
}


if (promise!=null) {
if (outcome.isSuccess()) promise.set(outcome.getNormal());
else {
try {
promise.setException(outcome.getAbnormal());
} catch (Error e) {
if (e==outcome.getAbnormal()) {
// SettableFuture tries to rethrow an Error, which we don't want.
// so prevent that from happening. I need to see if this behaviour
// affects other places that use SettableFuture
;
} else {
throw e;
}
if (promise!=null) {
if (outcome.isSuccess()) promise.set(outcome.getNormal());
else {
try {
promise.setException(outcome.getAbnormal());
} catch (Error e) {
if (e==outcome.getAbnormal()) {
// SettableFuture tries to rethrow an Error, which we don't want.
// so prevent that from happening. I need to see if this behaviour
// affects other places that use SettableFuture
;
} else {
throw e;
}
}
promise = null;
}

return outcome;
promise = null;
}

return outcome;
}

/**
Expand Down Expand Up @@ -259,6 +257,37 @@ public Future<Object> resume(Outcome v) {
return promise;
}

/**
* Stops the execution of this thread. If it's paused to wait for the completion of {@link StepExecution},
* call {@link StepExecution#stop(Throwable)} to give it a chance to clean up.
*
* <p>
* If the execution is not inside a step (meaning it's paused in a safe point), then have the CPS thread
* throw a given {@link Throwable} to break asap.
*/
@CpsVmThreadOnly
public void stop(Throwable t) {
StepExecution s = getStep(); // this is the part that should run in CpsVmThread
if (s == null) {
// if it's not running inside a StepExecution, we need to set an interrupt flag
// and interrupt at an earliest convenience
Outcome o = new Outcome(null, t);
if (resumeValue==null) {
resume(o);
} else {
// this thread was already resumed, so just overwrite the value with a Throwable
resumeValue = o;
}
return;
}

try {
s.stop(t);
} catch (Exception e) {
LOGGER.log(WARNING, "Failed to stop " + s, e);
}
}

public List<StackTraceElement> getStackTrace() {
return program.getStackTrace();
}
Expand Down

0 comments on commit f4a6607

Please sign in to comment.