Skip to content

Commit 4250456

Browse files
authored
Merge pull request #328 from jglick/findOriginFromBodyExecutionCallback
`ErrorAction.findOrigin` failures due to `ProxyException`
2 parents c7230bb + c952156 commit 4250456

File tree

3 files changed

+151
-14
lines changed

3 files changed

+151
-14
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<dependency>
7474
<groupId>io.jenkins.tools.bom</groupId>
7575
<artifactId>bom-2.426.x</artifactId>
76-
<version>2555.v3190a_8a_c60c6</version>
76+
<version>2745.vc7b_fe4c876fa_</version>
7777
<scope>import</scope>
7878
<type>pom</type>
7979
</dependency>

src/main/java/org/jenkinsci/plugins/workflow/actions/ErrorAction.java

+46-13
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import java.util.Objects;
3838
import java.util.Set;
3939
import java.util.UUID;
40+
import java.util.logging.Level;
41+
import java.util.logging.Logger;
4042
import jenkins.model.Jenkins;
4143
import org.apache.commons.io.output.NullOutputStream;
4244
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
@@ -53,23 +55,33 @@
5355
*/
5456
public class ErrorAction implements PersistentAction {
5557

58+
private static final Logger LOGGER = Logger.getLogger(ErrorAction.class.getName());
59+
5660
private final @NonNull Throwable error;
5761

5862
public ErrorAction(@NonNull Throwable error) {
63+
Throwable errorForAction = error;
5964
if (isUnserializableException(error, new HashSet<>())) {
60-
error = new ProxyException(error);
65+
LOGGER.log(Level.FINE, "sanitizing unserializable error", error);
66+
errorForAction = new ProxyException(error);
6167
} else if (error != null) {
6268
try {
6369
Jenkins.XSTREAM2.toXMLUTF8(error, new NullOutputStream());
6470
} catch (Exception x) {
71+
LOGGER.log(Level.FINE, "unable to serialize to XML", x);
6572
// Typically SecurityException from ClassFilter.
66-
error = new ProxyException(error);
73+
errorForAction = new ProxyException(error);
6774
}
6875
}
69-
this.error = error;
76+
this.error = errorForAction;
7077
String id = findId(error, new HashSet<>());
71-
if (id == null && error != null) {
72-
error.addSuppressed(new ErrorId());
78+
if (id == null) {
79+
var errorId = new ErrorId();
80+
errorForAction.addSuppressed(errorId);
81+
if (error != errorForAction) {
82+
// Make sure the original exception has the error ID, not just the copy here.
83+
error.addSuppressed(errorId);
84+
}
7385
}
7486
}
7587

@@ -170,28 +182,49 @@ public String getUrlName() {
170182
@Restricted(Beta.class)
171183
public static boolean equals(Throwable t1, Throwable t2) {
172184
if (t1 == t2) {
185+
LOGGER.fine(() -> "Same object: " + t1);
173186
return true;
174-
} else if (t1.getClass() != t2.getClass()) {
187+
}
188+
boolean noProxy = t1.getClass() != ProxyException.class && t2.getClass() != ProxyException.class;
189+
if (noProxy && t1.getClass() != t2.getClass()) {
190+
LOGGER.fine(() -> "Different types: " + t1.getClass() + " vs. " + t2.getClass());
175191
return false;
176-
} else if (!Objects.equals(t1.getMessage(), t2.getMessage())) {
192+
} else if (noProxy && !Objects.equals(t1.getMessage(), t2.getMessage())) {
193+
LOGGER.fine(() -> "Different messages: " + t1.getMessage() + " vs. " + t2.getMessage());
177194
return false;
178195
} else {
179196
String id1 = findId(t1, new HashSet<>());
180197
if (id1 != null) {
181-
return id1.equals(findId(t2, new HashSet<>()));
198+
String id2 = findId(t2, new HashSet<>());
199+
if (id1.equals(id2)) {
200+
LOGGER.fine(() -> "ErrorId matches: " + id1);
201+
return true;
202+
} else {
203+
LOGGER.fine(() -> "ErrorId mismatch: " + t1 + " " + id1 + " vs. " + t2 + " " + id2);
204+
return false;
205+
}
182206
}
183207
// No ErrorId, use a best-effort approach that doesn't work across restarts for exceptions thrown
184208
// synchronously from the CPS VM thread.
185209
// Check that stack traces match, but specifically avoid checking suppressed exceptions, which are often
186210
// modified after ErrorAction is written to disk when steps like parallel are involved.
187-
while (t1 != null && t2 != null) {
188-
if (!Arrays.equals(t1.getStackTrace(), t2.getStackTrace())) {
211+
var _t1 = t1;
212+
var _t2 = t2;
213+
while (_t1 != null && _t2 != null) {
214+
if (!Arrays.equals(_t1.getStackTrace(), _t2.getStackTrace())) {
215+
LOGGER.fine(() -> "Different stack traces between " + t1 + " vs. " + t2); // not showing details
189216
return false;
190217
}
191-
t1 = t1.getCause();
192-
t2 = t2.getCause();
218+
_t1 = _t1.getCause();
219+
_t2 = _t2.getCause();
220+
}
221+
if ((_t1 == null) == (_t2 == null)) {
222+
LOGGER.fine(() -> "Same stack traces in " + t1 + " vs. " + t2);
223+
return true;
224+
} else {
225+
LOGGER.fine(() -> "Different cause depths between " + t1 + " vs. " + t2);
226+
return false;
193227
}
194-
return (t1 == null) == (t2 == null);
195228
}
196229
}
197230

src/test/java/org/jenkinsci/plugins/workflow/actions/ErrorActionTest.java

+104
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,44 @@
4747
import org.jvnet.hudson.test.JenkinsSessionRule;
4848

4949
import groovy.lang.MissingMethodException;
50+
import hudson.FilePath;
5051
import hudson.Functions;
5152
import hudson.model.Result;
53+
import hudson.model.TaskListener;
5254
import hudson.remoting.ProxyException;
55+
import hudson.remoting.VirtualChannel;
56+
import java.io.File;
57+
import java.io.IOException;
58+
import java.util.Set;
59+
import java.util.logging.Level;
60+
import jenkins.MasterToSlaveFileCallable;
5361
import org.codehaus.groovy.runtime.NullObject;
62+
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
63+
import org.jenkinsci.plugins.workflow.steps.Step;
64+
import org.jenkinsci.plugins.workflow.steps.StepContext;
65+
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
66+
import org.jenkinsci.plugins.workflow.steps.StepExecution;
67+
import org.jenkinsci.plugins.workflow.steps.StepExecutions;
68+
import org.jvnet.hudson.test.InboundAgentRule;
69+
import org.jvnet.hudson.test.LoggerRule;
70+
import org.jvnet.hudson.test.TestExtension;
71+
import org.kohsuke.stapler.DataBoundConstructor;
5472

5573
/**
5674
* Tests for {@link ErrorAction}
5775
*/
5876
public class ErrorActionTest {
77+
5978
@ClassRule
6079
public static BuildWatcher buildWatcher = new BuildWatcher();
6180

6281
@Rule
6382
public JenkinsSessionRule rr = new JenkinsSessionRule();
6483

84+
@Rule public InboundAgentRule agents = new InboundAgentRule();
85+
86+
@Rule public LoggerRule logging = new LoggerRule().record(ErrorAction.class, Level.FINE);
87+
6588
private List<ErrorAction> extractErrorActions(FlowExecution exec) {
6689
List<ErrorAction> ret = new ArrayList<>();
6790

@@ -228,6 +251,87 @@ public static class X extends Exception {
228251
});
229252
}
230253

254+
@Test public void findOriginFromBodyExecutionCallback() throws Throwable {
255+
rr.then(r -> {
256+
agents.createAgent(r, "remote");
257+
var p = r.createProject(WorkflowJob.class);
258+
p.setDefinition(new CpsFlowDefinition("callsFindOrigin {node('remote') {fails()}}", true));
259+
var b = p.scheduleBuild2(0).waitForStart();
260+
r.waitForMessage("Acting slowly in ", b);
261+
agents.stop("remote");
262+
r.assertBuildStatus(Result.FAILURE, r.waitForCompletion(b));
263+
r.assertLogContains("Found in: fails", b);
264+
});
265+
}
266+
public static final class WrapperStep extends Step {
267+
@DataBoundConstructor public WrapperStep() {}
268+
@Override public StepExecution start(StepContext context) throws Exception {
269+
return new ExecutionImpl(context);
270+
}
271+
private static final class ExecutionImpl extends StepExecution {
272+
ExecutionImpl(StepContext context) {
273+
super(context);
274+
}
275+
@Override public boolean start() throws Exception {
276+
getContext().newBodyInvoker().withCallback(new Callback()).start();
277+
return false;
278+
}
279+
}
280+
private static class Callback extends BodyExecutionCallback {
281+
@Override public void onSuccess(StepContext context, Object result) {
282+
context.onSuccess(result);
283+
}
284+
@Override
285+
public void onFailure(StepContext context, Throwable t) {
286+
try {
287+
var l = context.get(TaskListener.class);
288+
Functions.printStackTrace(t, l.error("Original failure:"));
289+
l.getLogger().println("Found in: " + ErrorAction.findOrigin(t, context.get(FlowExecution.class)).getDisplayFunctionName());
290+
} catch (Exception x) {
291+
assert false : x;
292+
}
293+
context.onFailure(t);
294+
}
295+
}
296+
@TestExtension("findOriginFromBodyExecutionCallback") public static final class DescriptorImpl extends StepDescriptor {
297+
@Override public String getFunctionName() {
298+
return "callsFindOrigin";
299+
}
300+
@Override public Set<? extends Class<?>> getRequiredContext() {
301+
return Set.of();
302+
}
303+
@Override public boolean takesImplicitBlockArgument() {
304+
return true;
305+
}
306+
}
307+
}
308+
public static final class FailingStep extends Step {
309+
@DataBoundConstructor public FailingStep() {}
310+
@Override public StepExecution start(StepContext context) throws Exception {
311+
return StepExecutions.synchronousNonBlockingVoid(context, c -> c.get(FilePath.class).act(new Sleep(c.get(TaskListener.class))));
312+
}
313+
private static final class Sleep extends MasterToSlaveFileCallable<Void> {
314+
private final TaskListener l;
315+
Sleep(TaskListener l) {
316+
this.l = l;
317+
}
318+
@Override public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
319+
l.getLogger().println("Acting slowly in " + f);
320+
l.getLogger().flush();
321+
Thread.sleep(Long.MAX_VALUE);
322+
return null;
323+
}
324+
}
325+
@TestExtension("findOriginFromBodyExecutionCallback") public static final class DescriptorImpl extends StepDescriptor {
326+
@Override public String getFunctionName() {
327+
return "fails";
328+
}
329+
@Override public Set<? extends Class<?>> getRequiredContext() {
330+
return Set.of(FilePath.class);
331+
}
332+
}
333+
}
334+
231335
@Test public void cyclicErrorsAreSupported() throws Throwable {
232336
Exception cyclic1 = new Exception();
233337
Exception cyclic2 = new Exception(cyclic1);

0 commit comments

Comments
 (0)