|
24 | 24 |
|
25 | 25 | package org.jenkinsci.plugins.workflow.flow; |
26 | 26 |
|
| 27 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 28 | +import static org.hamcrest.Matchers.containsString; |
| 29 | +import static org.hamcrest.Matchers.hasItem; |
27 | 30 | import static org.junit.Assert.assertNotNull; |
28 | 31 |
|
| 32 | +import hudson.AbortException; |
29 | 33 | import hudson.model.ParametersAction; |
30 | 34 | import hudson.model.ParametersDefinitionProperty; |
| 35 | +import hudson.model.Result; |
31 | 36 | import hudson.model.StringParameterDefinition; |
32 | 37 | import hudson.model.StringParameterValue; |
| 38 | +import hudson.model.TaskListener; |
33 | 39 | import hudson.model.queue.QueueTaskFuture; |
| 40 | +import java.io.Serializable; |
| 41 | +import java.time.Duration; |
| 42 | +import java.time.Instant; |
| 43 | +import java.util.Collections; |
| 44 | +import java.util.Set; |
| 45 | +import java.util.function.Supplier; |
34 | 46 | import java.util.logging.Level; |
| 47 | +import org.hamcrest.Matcher; |
35 | 48 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; |
36 | 49 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; |
37 | 50 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; |
| 51 | +import org.jenkinsci.plugins.workflow.steps.Step; |
| 52 | +import org.jenkinsci.plugins.workflow.steps.StepContext; |
| 53 | +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; |
| 54 | +import org.jenkinsci.plugins.workflow.steps.StepExecution; |
| 55 | +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; |
38 | 56 | import org.junit.ClassRule; |
39 | 57 | import org.junit.Test; |
40 | 58 | import org.junit.Rule; |
41 | 59 | import org.jvnet.hudson.test.BuildWatcher; |
42 | 60 | import org.jvnet.hudson.test.Issue; |
43 | 61 | import org.jvnet.hudson.test.LoggerRule; |
44 | 62 | import org.jvnet.hudson.test.JenkinsSessionRule; |
| 63 | +import org.jvnet.hudson.test.TestExtension; |
| 64 | +import org.kohsuke.stapler.DataBoundConstructor; |
45 | 65 |
|
46 | 66 | public class FlowExecutionListTest { |
47 | 67 |
|
@@ -79,4 +99,113 @@ public class FlowExecutionListTest { |
79 | 99 | }); |
80 | 100 | } |
81 | 101 |
|
| 102 | + @Test public void forceLoadRunningExecutionsAfterRestart() throws Throwable { |
| 103 | + logging.capture(50); |
| 104 | + sessions.then(r -> { |
| 105 | + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 106 | + p.setDefinition(new CpsFlowDefinition("semaphore('wait')", true)); |
| 107 | + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); |
| 108 | + SemaphoreStep.waitForStart("wait/1", b); |
| 109 | + }); |
| 110 | + sessions.then(r -> { |
| 111 | + /* |
| 112 | + Make sure that the build gets loaded automatically by FlowExecutionList$ItemListenerImpl before we load it explictly. |
| 113 | + Expected call stack for resuming a Pipelines and its steps: |
| 114 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$ResumeStepExecutionListener$1.onSuccess(FlowExecutionList.java:250) |
| 115 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$ResumeStepExecutionListener$1.onSuccess(FlowExecutionList.java:247) |
| 116 | + at com.google.common.util.concurrent.Futures$6.run(Futures.java:975) |
| 117 | + at org.jenkinsci.plugins.workflow.flow.DirectExecutor.execute(DirectExecutor.java:33) |
| 118 | + ... Guava Futures API internals ... |
| 119 | + at com.google.common.util.concurrent.Futures.addCallback(Futures.java:985) |
| 120 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$ResumeStepExecutionListener.onResumed(FlowExecutionList.java:247) |
| 121 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionListener.fireResumed(FlowExecutionListener.java:84) |
| 122 | + at org.jenkinsci.plugins.workflow.job.WorkflowRun.onLoad(WorkflowRun.java:528) |
| 123 | + at hudson.model.RunMap.retrieve(RunMap.java:225) |
| 124 | + ... RunMap internals ... |
| 125 | + at hudson.model.RunMap.getById(RunMap.java:205) |
| 126 | + at org.jenkinsci.plugins.workflow.job.WorkflowRun$Owner.run(WorkflowRun.java:937) |
| 127 | + at org.jenkinsci.plugins.workflow.job.WorkflowRun$Owner.get(WorkflowRun.java:948) |
| 128 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$1.computeNext(FlowExecutionList.java:65) |
| 129 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$1.computeNext(FlowExecutionList.java:57) |
| 130 | + at com.google.common.collect.AbstractIterator.tryToComputeNext(AbstractIterator.java:143) |
| 131 | + at com.google.common.collect.AbstractIterator.hasNext(AbstractIterator.java:138) |
| 132 | + at org.jenkinsci.plugins.workflow.flow.FlowExecutionList$ItemListenerImpl.onLoaded(FlowExecutionList.java:175) |
| 133 | + at jenkins.model.Jenkins.<init>(Jenkins.java:1019) |
| 134 | + */ |
| 135 | + waitFor(logging::getMessages, hasItem(containsString("Will resume [org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep"))); |
| 136 | + WorkflowJob p = r.jenkins.getItemByFullName("p", WorkflowJob.class); |
| 137 | + SemaphoreStep.success("wait/1", null); |
| 138 | + WorkflowRun b = p.getBuildByNumber(1); |
| 139 | + r.waitForCompletion(b); |
| 140 | + r.assertBuildStatus(Result.SUCCESS, b); |
| 141 | + }); |
| 142 | + } |
| 143 | + |
| 144 | + @Issue("JENKINS-67164") |
| 145 | + @Test public void resumeStepExecutions() throws Throwable { |
| 146 | + sessions.then(r -> { |
| 147 | + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 148 | + p.setDefinition(new CpsFlowDefinition("noResume()", true)); |
| 149 | + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); |
| 150 | + r.waitForMessage("Starting non-resumable step", b); |
| 151 | + // TODO: Unclear how this might happen in practice. |
| 152 | + FlowExecutionList.get().unregister(b.asFlowExecutionOwner()); |
| 153 | + }); |
| 154 | + sessions.then(r -> { |
| 155 | + WorkflowJob p = r.jenkins.getItemByFullName("p", WorkflowJob.class); |
| 156 | + WorkflowRun b = p.getBuildByNumber(1); |
| 157 | + r.waitForCompletion(b); |
| 158 | + r.assertBuildStatus(Result.FAILURE, b); |
| 159 | + r.assertLogContains("Unable to resume NonResumableStep", b); |
| 160 | + }); |
| 161 | + } |
| 162 | + |
| 163 | + public static class NonResumableStep extends Step implements Serializable { |
| 164 | + public static final long serialVersionUID = 1L; |
| 165 | + @DataBoundConstructor |
| 166 | + public NonResumableStep() { } |
| 167 | + @Override |
| 168 | + public StepExecution start(StepContext sc) { |
| 169 | + return new ExecutionImpl(sc); |
| 170 | + } |
| 171 | + |
| 172 | + private static class ExecutionImpl extends StepExecution implements Serializable { |
| 173 | + public static final long serialVersionUID = 1L; |
| 174 | + private ExecutionImpl(StepContext sc) { |
| 175 | + super(sc); |
| 176 | + } |
| 177 | + @Override |
| 178 | + public boolean start() throws Exception { |
| 179 | + getContext().get(TaskListener.class).getLogger().println("Starting non-resumable step"); |
| 180 | + return false; |
| 181 | + } |
| 182 | + @Override |
| 183 | + public void onResume() { |
| 184 | + getContext().onFailure(new AbortException("Unable to resume NonResumableStep")); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + @TestExtension public static class DescriptorImpl extends StepDescriptor { |
| 189 | + @Override |
| 190 | + public Set<? extends Class<?>> getRequiredContext() { |
| 191 | + return Collections.singleton(TaskListener.class); |
| 192 | + } |
| 193 | + @Override |
| 194 | + public String getFunctionName() { |
| 195 | + return "noResume"; |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + /** |
| 201 | + * Wait up to 5 seconds for the given supplier to return a matching value. |
| 202 | + */ |
| 203 | + private static <T> void waitFor(Supplier<T> valueSupplier, Matcher<T> matcher) throws InterruptedException { |
| 204 | + Instant end = Instant.now().plus(Duration.ofSeconds(5)); |
| 205 | + while (!matcher.matches(valueSupplier.get()) && Instant.now().isBefore(end)) { |
| 206 | + Thread.sleep(100L); |
| 207 | + } |
| 208 | + assertThat("Matcher should have matched after 5s", valueSupplier.get(), matcher); |
| 209 | + } |
| 210 | + |
82 | 211 | } |
0 commit comments