diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java index 8211b7dbb..1d4a2fa19 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java @@ -732,51 +732,54 @@ private synchronized String getHeadsAsString() { @GuardedBy("this") void createPlaceholderNodes(Throwable failureReason) throws Exception { synchronized (this) { - this.done = true; - - if (this.owner != null) { - // Ensure that the Run is marked as completed (failed) if it isn't already so it won't show as running - Queue.Executable ex = owner.getExecutable(); - if (ex instanceof Run) { - Result res = ((Run) ex).getResult(); - setResult(res != null ? res : Result.FAILURE); + try { + if (this.owner != null) { + // Ensure that the Run is marked as completed (failed) if it isn't already so it won't show as + // running + Queue.Executable ex = owner.getExecutable(); + if (ex instanceof Run) { + Result res = ((Run) ex).getResult(); + setResult(res != null ? res : Result.FAILURE); + } } - } - programPromise = - Futures.immediateFailedFuture(new IllegalStateException("Failed loading heads", failureReason)); - LOGGER.log(Level.INFO, "Creating placeholder flownodes for execution: " + this); - if (this.owner != null) { - try { - owner.getListener() - .getLogger() - .println("Creating placeholder flownodes because failed loading originals."); - } catch (Exception ex) { - // It's okay to fail to log + programPromise = + Futures.immediateFailedFuture(new IllegalStateException("Failed loading heads", failureReason)); + LOGGER.log(Level.INFO, "Creating placeholder flownodes for execution: " + this); + if (this.owner != null) { + try { + owner.getListener() + .getLogger() + .println("Creating placeholder flownodes because failed loading originals."); + } catch (Exception ex) { + // It's okay to fail to log + } } - } - // Switch to fallback storage so we don't delete original node data - this.storageDir = (this.storageDir != null) ? this.storageDir + "-fallback" : "workflow-fallback"; - this.storage = createStorage(); // Empty storage - - // Clear out old start nodes and heads - this.startNodes = new Stack<>(); - FlowHead head = new FlowHead(this); - this.heads = new TreeMap<>(); - heads.put(head.getId(), head); - FlowStartNode start = new FlowStartNode(this, iotaStr()); - head.newStartNode(start); - - // Create end - FlowNode end = new FlowEndNode( - this, - iotaStr(), - (FlowStartNode) startNodes.pop(), - result, - getCurrentHeads().toArray(new FlowNode[0])); - end.addAction(new ErrorAction(failureReason)); - head.setNewHead(end); + // Switch to fallback storage so we don't delete original node data + this.storageDir = (this.storageDir != null) ? this.storageDir + "-fallback" : "workflow-fallback"; + this.storage = createStorage(); // Empty storage + + // Clear out old start nodes and heads + this.startNodes = new Stack<>(); + FlowHead head = new FlowHead(this); + this.heads = new TreeMap<>(); + heads.put(head.getId(), head); + FlowStartNode start = new FlowStartNode(this, iotaStr()); + head.newStartNode(start); + + // Create end + FlowNode end = new FlowEndNode( + this, + iotaStr(), + (FlowStartNode) startNodes.pop(), + result, + getCurrentHeads().toArray(new FlowNode[0])); + end.addAction(new ErrorAction(failureReason)); + head.setNewHead(end); + } finally { + this.done = true; + } } saveOwner(); } @@ -936,6 +939,11 @@ public void onSuccess(Unmarshaller u) { PROGRAM_STATE_SERIALIZATION.set(CpsFlowExecution.this); try { CpsThreadGroup g = (CpsThreadGroup) u.readObject(); + if (!g.getThreads().iterator().hasNext() && !isComplete()) { + LOGGER.log(Level.WARNING, "Loaded program for {0} contains no CPS threads", owner); + loadProgramFailed(new AbortException("Loaded program contains no CPS threads"), result); + return; + } result.set(g); pausedWhenLoaded = g.isPaused(); g.pause(false); diff --git a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/PersistenceProblemsTest.java b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/PersistenceProblemsTest.java index 6200fd71a..80e537bec 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/PersistenceProblemsTest.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/PersistenceProblemsTest.java @@ -9,17 +9,22 @@ import com.google.common.util.concurrent.ListenableFuture; import hudson.model.Queue; import hudson.model.Result; +import hudson.model.Run; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.file.Files; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode; import org.jenkinsci.plugins.workflow.flow.FlowDurabilityHint; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.flow.FlowExecutionList; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; +import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.graph.FlowStartNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.job.properties.DurabilityHintJobProperty; @@ -106,6 +111,12 @@ static void assertResultMatchExecutionAndRun(WorkflowRun run, Result[] execution Assert.assertEquals(executionAndBuildResult[1], run.getResult()); } + private static void setField(Object target, Class declaringClass, String name, Object value) throws Exception { + Field field = declaringClass.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + /** Create and run a basic build before we mangle its persisted contents. Stores job number to jobIdNumber index 0. */ private static WorkflowRun runBasicBuild(JenkinsRule j, String jobName, int[] jobIdNumber, FlowDurabilityHint hint) throws Exception { @@ -231,6 +242,47 @@ public void completedButWrongDoneStatus() throws Exception { }); } + @Test + public void inProgressWithStepEndHeadButNoCpsThreads() throws Exception { + final int[] build = new int[1]; + story.thenWithHardShutdown(j -> { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, DEFAULT_JOBNAME); + job.setDefinition(new CpsFlowDefinition("stage('x') { echo 'doSomething' }", true)); + WorkflowRun run = j.buildAndAssertSuccess(job); + build[0] = run.getNumber(); + assertCompletedCleanly(run, true); + CpsFlowExecution cpsExec = (CpsFlowExecution) run.getExecution(); + FlowNode stepEnd = new DepthFirstScanner().findFirstMatch(cpsExec, n -> n instanceof StepEndNode); + Assert.assertNotNull("expected completed build to contain a StepEndNode", stepEnd); + + FlowHead head = cpsExec.getFirstHead(); + Assert.assertNotNull("expected a persisted FlowHead", head); + head.head = stepEnd; + cpsExec.done = false; + CpsThreadGroup emptyProgram = new CpsThreadGroup(cpsExec); + try { + emptyProgram + .runner + .submit(() -> { + emptyProgram.saveProgram(cpsExec.getProgramDataFile()); + return null; + }) + .get(); + } finally { + emptyProgram.shutdown(); + } + setField(run, WorkflowRun.class, "completed", false); + setField(run, Run.class, "result", null); + run.save(); + }); + story.then(j -> { + WorkflowJob r = j.jenkins.getItemByFullName(DEFAULT_JOBNAME, WorkflowJob.class); + WorkflowRun run = r.getBuildByNumber(build[0]); + assertCompletedCleanly(run, false); + Assert.assertEquals(Result.FAILURE, run.getResult()); + }); + } + @Test public void inProgressNormal() throws Exception { final int[] build = new int[1];