Skip to content

Commit e65bfa6

Browse files
authored
Merge pull request #1067 from jglick/refersToCycle
`StackOverflowError` in `CpsStepContext.refersTo`
2 parents 4a1fd14 + ad3a522 commit e65bfa6

File tree

2 files changed

+50
-16
lines changed

2 files changed

+50
-16
lines changed

plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsStepContext.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@
5858
import java.io.IOException;
5959
import java.io.Serializable;
6060
import java.util.ArrayList;
61+
import java.util.Collections;
62+
import java.util.IdentityHashMap;
6163
import java.util.List;
64+
import java.util.Set;
6265
import java.util.concurrent.Callable;
6366
import java.util.concurrent.ExecutionException;
6467
import java.util.concurrent.ExecutorService;
@@ -346,14 +349,14 @@ private void completed(@NonNull Outcome newOutcome) {
346349
LOGGER.log(Level.FINE, "earlier success: {0}", outcome.getNormal());
347350
}
348351
}
349-
if (failure != null && earlierFailure != null && !refersTo(failure, earlierFailure)) {
352+
if (failure != null && earlierFailure != null && !refersTo(failure, earlierFailure, Collections.newSetFromMap(new IdentityHashMap<>()))) {
350353
earlierFailure.addSuppressed(failure);
351354
}
352355
}
353356
}
354357

355-
private static boolean refersTo(Throwable t1, Throwable t2) {
356-
return t1 == t2 || t1.getCause() != null && refersTo(t1.getCause(), t2) || Stream.of(t1.getSuppressed()).anyMatch(t3 -> refersTo(t3, t2));
358+
private static boolean refersTo(Throwable t1, Throwable t2, Set<Throwable> checked) {
359+
return checked.add(t1) && (t1 == t2 || t1.getCause() != null && refersTo(t1.getCause(), t2, checked) || Stream.of(t1.getSuppressed()).anyMatch(t3 -> refersTo(t3, t2, checked)));
357360
}
358361

359362
/**

plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsStepContextTest.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import static org.hamcrest.Matchers.equalTo;
3434
import static org.hamcrest.Matchers.hasItem;
3535
import static org.hamcrest.Matchers.not;
36+
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
3637

3738
public class CpsStepContextTest {
3839
@Rule
@@ -82,19 +83,6 @@ public void executionStartExceptionNotLeakClosures() throws Exception {
8283
assertThat(logger.getMessages(), not(hasItem(containsString("Stale closure"))));
8384
}
8485

85-
@Issue("JENKINS-75067")
86-
@Test
87-
public void executionWithBodyRunningSyncNotLeakClosures() throws Exception {
88-
logger.record(CpsThreadGroup.class, Level.WARNING).capture(10);
89-
WorkflowJob job = r.createProject(WorkflowJob.class, "p");
90-
job.setDefinition(new CpsFlowDefinition("def r = passthrough {}; echo r", true));
91-
92-
WorkflowRun build = r.buildAndAssertSuccess(job);
93-
r.assertLogContains("hooray", build);
94-
assertThat(ClosureCounter.get().closureCount, equalTo(0));
95-
assertThat(logger.getMessages(), not(hasItem(containsString("Stale closure"))));
96-
}
97-
9886
public static class BadBlockStep extends Step {
9987

10088
@DataBoundConstructor
@@ -127,6 +115,19 @@ public boolean takesImplicitBlockArgument() {
127115
}
128116
}
129117

118+
@Issue("JENKINS-75067")
119+
@Test
120+
public void executionWithBodyRunningSyncNotLeakClosures() throws Exception {
121+
logger.record(CpsThreadGroup.class, Level.WARNING).capture(10);
122+
WorkflowJob job = r.createProject(WorkflowJob.class, "p");
123+
job.setDefinition(new CpsFlowDefinition("def r = passthrough {}; echo r", true));
124+
125+
WorkflowRun build = r.buildAndAssertSuccess(job);
126+
r.assertLogContains("hooray", build);
127+
assertThat(ClosureCounter.get().closureCount, equalTo(0));
128+
assertThat(logger.getMessages(), not(hasItem(containsString("Stale closure"))));
129+
}
130+
130131
public static class PassthroughStep extends Step {
131132

132133
@DataBoundConstructor
@@ -181,4 +182,34 @@ static ClosureCounter get() {
181182
return ExtensionList.lookupSingleton(ClosureCounter.class);
182183
}
183184
}
185+
186+
@Test public void refersToCycle() throws Exception {
187+
logger.record(CpsStepContext.class, Level.ALL);
188+
var p = r.createProject(WorkflowJob.class, "p");
189+
p.setDefinition(new CpsFlowDefinition("refersToCycle()", true));
190+
r.assertLogNotContains("StackOverflowError", r.buildAndAssertStatus(Result.UNSTABLE, p));
191+
}
192+
public static final class RefersToCycleStep extends Step {
193+
@DataBoundConstructor public RefersToCycleStep() {}
194+
@Override public StepExecution start(StepContext context) throws Exception {
195+
return StepExecutions.synchronousNonBlockingVoid(context, ctx -> {
196+
var t1 = new IllegalStateException("extra");
197+
var t2 = new FlowInterruptedException(Result.UNSTABLE, false);
198+
var t3 = new IllegalArgumentException("whatever");
199+
t1.addSuppressed(t3);
200+
t3.addSuppressed(t1);
201+
ctx.onFailure(t2);
202+
ctx.onFailure(t1);
203+
});
204+
}
205+
@TestExtension("refersToCycle") public static final class DescriptorImpl extends StepDescriptor {
206+
@Override public String getFunctionName() {
207+
return "refersToCycle";
208+
}
209+
@Override public Set<? extends Class<?>> getRequiredContext() {
210+
return Set.of();
211+
}
212+
}
213+
}
214+
184215
}

0 commit comments

Comments
 (0)