Skip to content

Commit 07b215a

Browse files
authored
Merge pull request jenkinsci#278 from dwnusbaum/pickleresolver-timeout
Add a timeout for pickle resolution to prevent indefinitely stuck builds
2 parents 45b131a + 51cf5f3 commit 07b215a

File tree

2 files changed

+98
-7
lines changed

2 files changed

+98
-7
lines changed

src/main/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/PickleResolver.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,38 @@
2424

2525
package org.jenkinsci.plugins.workflow.support.pickles.serialization;
2626

27-
import org.jenkinsci.plugins.workflow.pickles.Pickle;
28-
import org.jenkinsci.plugins.workflow.support.concurrent.Futures;
2927
import com.google.common.base.Function;
28+
import com.google.common.util.concurrent.Futures;
3029
import com.google.common.util.concurrent.ListenableFuture;
31-
import org.jboss.marshalling.ObjectResolver;
32-
30+
import com.google.common.util.concurrent.MoreExecutors;
31+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3332
import java.util.ArrayList;
3433
import java.util.Collection;
3534
import java.util.List;
35+
import java.util.concurrent.TimeUnit;
36+
import jenkins.util.SystemProperties;
37+
import jenkins.util.Timer;
38+
import org.jboss.marshalling.ObjectResolver;
3639
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
40+
import org.jenkinsci.plugins.workflow.pickles.Pickle;
41+
import org.kohsuke.accmod.Restricted;
42+
import org.kohsuke.accmod.restrictions.NoExternalUse;
3743

3844
/**
3945
* {@link ObjectResolver} that resolves {@link DryCapsule} to unpickled objects.
4046
*
4147
* @author Kohsuke Kawaguchi
4248
*/
4349
public class PickleResolver implements ObjectResolver {
50+
51+
/**
52+
* Pickle resolution will fail automatically after this many seconds.
53+
* <p>This is intended to prevent Pipeline builds from hanging forever in unusual cases.
54+
*/
55+
@Restricted(NoExternalUse.class)
56+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for modification from script console")
57+
public static long RESOLUTION_TIMEOUT_SECONDS = SystemProperties.getLong(PickleResolver.class + ".RESOLUTION_TIMEOUT_SECONDS", TimeUnit.HOURS.toSeconds(1));
58+
4459
/**
4560
* Persisted forms of the stateful objects.
4661
*/
@@ -83,7 +98,7 @@ public ListenableFuture<PickleResolver> rehydrate(Collection<ListenableFuture<?>
8398
// TODO log("rehydrating " + r);
8499
ListenableFuture<?> future;
85100
try {
86-
future = r.rehydrate(owner);
101+
future = Futures.withTimeout(r.rehydrate(owner), RESOLUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS, Timer.get());
87102
} catch (RuntimeException x) {
88103
future = Futures.immediateFailedFuture(x);
89104
}
@@ -93,7 +108,7 @@ public ListenableFuture<PickleResolver> rehydrate(Collection<ListenableFuture<?>
93108
// TODO log("rehydrated to " + input);
94109
return input;
95110
}
96-
}));
111+
}, MoreExecutors.directExecutor()));
97112
}
98113

99114
ListenableFuture<List<Object>> all = Futures.allAsList(members);
@@ -104,7 +119,7 @@ public PickleResolver apply(List<Object> input) {
104119
values = input;
105120
return PickleResolver.this;
106121
}
107-
});
122+
}, MoreExecutors.directExecutor());
108123
}
109124

110125
@Override
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.jenkinsci.plugins.workflow.support.pickles.serialization;
2+
3+
import com.google.common.util.concurrent.ListenableFuture;
4+
import hudson.model.Result;
5+
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
6+
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
7+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
8+
import org.jenkinsci.plugins.workflow.pickles.Pickle;
9+
import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory;
10+
import org.jenkinsci.plugins.workflow.support.pickles.TryRepeatedly;
11+
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
12+
import org.junit.ClassRule;
13+
import org.junit.Rule;
14+
import org.junit.Test;
15+
import org.jvnet.hudson.test.BuildWatcher;
16+
import org.jvnet.hudson.test.FlagRule;
17+
import org.jvnet.hudson.test.JenkinsSessionRule;
18+
import org.jvnet.hudson.test.TestExtension;
19+
20+
public class PickleResolverTest {
21+
@ClassRule
22+
public static BuildWatcher watcher = new BuildWatcher();
23+
24+
@Rule
25+
public JenkinsSessionRule sessions = new JenkinsSessionRule();
26+
27+
@Rule
28+
public FlagRule<Long> resetPickleResolutionTimeout = new FlagRule<>(() -> PickleResolver.RESOLUTION_TIMEOUT_SECONDS, x -> PickleResolver.RESOLUTION_TIMEOUT_SECONDS = x);
29+
30+
@Test
31+
public void timeout() throws Throwable {
32+
sessions.then(r -> {
33+
var p = r.jenkins.createProject(WorkflowJob.class, "stuckPickle");
34+
p.setDefinition(new CpsFlowDefinition(
35+
"def x = new org.jenkinsci.plugins.workflow.support.pickles.serialization.PickleResolverTest.StuckPickle.Marker()\n" +
36+
"semaphore 'wait'\n" +
37+
"echo x.getClass().getName()", false));
38+
var b = p.scheduleBuild2(0).waitForStart();
39+
SemaphoreStep.waitForStart("wait/1", b);
40+
});
41+
PickleResolver.RESOLUTION_TIMEOUT_SECONDS = 3;
42+
sessions.then(r -> {
43+
var p = r.jenkins.getItemByFullName("stuckPickle", WorkflowJob.class);
44+
var b = p.getBuildByNumber(1);
45+
r.assertBuildStatus(Result.FAILURE, r.waitForCompletion(b));
46+
r.assertLogContains("Timed out: StuckPickle", b);
47+
});
48+
}
49+
50+
public static class StuckPickle extends Pickle {
51+
@Override
52+
public ListenableFuture<Marker> rehydrate(FlowExecutionOwner owner) {
53+
return new TryRepeatedly<Marker>(1) {
54+
@Override
55+
protected Marker tryResolve() {
56+
return null;
57+
}
58+
@Override protected FlowExecutionOwner getOwner() {
59+
return owner;
60+
}
61+
@Override public String toString() {
62+
return "StuckPickle for " + owner;
63+
}
64+
};
65+
}
66+
67+
public static class Marker {}
68+
69+
@TestExtension("timeout")
70+
public static final class Factory extends SingleTypedPickleFactory<Marker> {
71+
@Override protected Pickle pickle(Marker object) {
72+
return new StuckPickle();
73+
}
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)