diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorCondition.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorCondition.java index 1410b47c..c1c5f657 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorCondition.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorCondition.java @@ -53,7 +53,11 @@ public final class AgentErrorCondition extends ErrorCondition { @DataBoundConstructor public AgentErrorCondition() {} @Override public boolean test(Throwable t, StepContext context) throws IOException, InterruptedException { - if (t instanceof FlowInterruptedException && ((FlowInterruptedException) t).getCauses().stream().anyMatch(ExecutorStepExecution.RemovedNodeCause.class::isInstance)) { + if (t instanceof AgentOfflineException) { + return true; + } + if (t instanceof FlowInterruptedException && ((FlowInterruptedException) t).getCauses().stream().anyMatch( + c -> c instanceof ExecutorStepExecution.RemovedNodeCause || c instanceof ExecutorStepExecution.QueueTaskCancelled)) { return true; } if (isClosedChannel(t)) { diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentOfflineException.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentOfflineException.java new file mode 100644 index 00000000..0c832a65 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/AgentOfflineException.java @@ -0,0 +1,40 @@ +/* + * The MIT License + * + * Copyright 2022 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.support.steps; + +import hudson.FilePath; +import java.io.IOException; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +/** + * Inability to satisfy some request (such as {@link StepContext#get} on {@link FilePath}) because an agent is offline. + */ +final class AgentOfflineException extends IOException { + + AgentOfflineException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/FilePathDynamicContext.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/FilePathDynamicContext.java index 4e2cc181..ac7ac83c 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/FilePathDynamicContext.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/FilePathDynamicContext.java @@ -63,7 +63,7 @@ if (f != null) { LOGGER.log(Level.FINE, "serving {0}:{1}", new Object[] {r.slave, r.path}); } else { - IOException e = new IOException("Unable to create live FilePath for " + r.slave); + AgentOfflineException e = new AgentOfflineException("Unable to create live FilePath for " + r.slave); Computer c = Jenkins.get().getComputer(r.slave); if (c != null) { for (Computer.TerminationRequest tr : c.getTerminatedBy()) { diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/RetryExecutorStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorConditionTest.java similarity index 79% rename from src/test/java/org/jenkinsci/plugins/workflow/support/steps/RetryExecutorStepTest.java rename to src/test/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorConditionTest.java index b86d1f05..cf07cf4e 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/RetryExecutorStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/AgentErrorConditionTest.java @@ -26,8 +26,12 @@ import hudson.Functions; import hudson.Launcher; +import hudson.model.Label; +import hudson.model.Queue; +import hudson.model.Result; import hudson.model.Slave; import hudson.model.TaskListener; +import hudson.slaves.OfflineCause; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -49,9 +53,8 @@ import org.jenkinsci.plugins.workflow.steps.StepExecutions; import org.jenkinsci.plugins.workflow.steps.SynchronousResumeNotSupportedErrorCondition; import org.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep; -import org.jenkinsci.plugins.workflow.support.steps.AgentErrorCondition; -import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import static org.junit.Assert.assertEquals; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Rule; @@ -67,14 +70,14 @@ /** * Tests of retrying {@code node} blocks. */ -public class RetryExecutorStepTest { +@Issue("JENKINS-49707") +public class AgentErrorConditionTest { @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @Rule public JenkinsSessionRule sessions = new JenkinsSessionRule(); @Rule public InboundAgentRule inboundAgents = new InboundAgentRule(); @Rule public LoggerRule logging = new LoggerRule(); - @Issue("JENKINS-49707") @Test public void retryNodeBlock() throws Throwable { sessions.then(r -> { logging.record(DurableTaskStep.class, Level.FINE).record(FileMonitoringTask.class, Level.FINE).record(ExecutorStepExecution.class, Level.FINE); @@ -101,7 +104,6 @@ public class RetryExecutorStepTest { }); } - @Issue("JENKINS-49707") @Test public void retryNodeBlockSynch() throws Throwable { sessions.then(r -> { logging.record(ExecutorStepExecution.class, Level.FINE); @@ -157,8 +159,72 @@ public static final class HangStep extends Step { } } + @Test public void agentOfflineWhenStartingStep() throws Throwable { + sessions.then(r -> { + Slave s = r.createSlave(Label.get("remote")); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "retry(count: 2, conditions: [custom()]) {\n" + + " node('remote') {\n" + + " semaphore 'wait'\n" + + " pwd()\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + s.toComputer().disconnect(new OfflineCause.UserCause(null, null)); + while (s.toComputer().isOnline()) { + Thread.sleep(100); + } + SemaphoreStep.success("wait/1", null); + r.waitForMessage(RetryThis.MESSAGE, b); + SemaphoreStep.success("wait/2", null); + s.toComputer().connect(false); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + }); + } + + @Test public void queueTaskCancelled() throws Throwable { + sessions.then(r -> { + Slave s = r.createSlave(Label.get("remote")); + s.toComputer().setTemporarilyOffline(true, new OfflineCause.UserCause(null, null)); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "retry(count: 2, conditions: [custom()]) {\n" + + " node('remote') {\n" + + " isUnix()\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + r.waitForMessage("Still waiting to schedule task", b); + Queue.Item[] items = Queue.getInstance().getItems(); + assertEquals(1, items.length); + Queue.getInstance().cancel(items[0]); + r.waitForMessage(RetryThis.MESSAGE, b); + s.toComputer().setTemporarilyOffline(false, null); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + }); + } + + @Test public void overallBuildCancelIgnored() throws Throwable { + sessions.then(r -> { + r.createSlave(Label.get("remote")); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "retry(count: 2, conditions: [custom()]) {\n" + + " node('remote') {\n" + + " semaphore 'wait'\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + b.getExecutor().interrupt(); + r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b)); + r.assertLogNotContains(RetryThis.MESSAGE, b); + }); + } + @Ignore("TODO pending https://github.com/jenkinsci/workflow-durable-task-step-plugin/pull/180") - @Issue("JENKINS-49707") @Test public void retryNewStepAcrossRestarts() throws Throwable { logging.record(DurableTaskStep.class, Level.FINE).record(FileMonitoringTask.class, Level.FINE).record(ExecutorStepExecution.class, Level.FINE); sessions.then(r -> { @@ -192,7 +258,7 @@ public static final class HangStep extends Step { } @Ignore("TODO pending https://github.com/jenkinsci/workflow-durable-task-step-plugin/pull/180") - @Issue({"JENKINS-49707", "JENKINS-30383"}) + @Issue("JENKINS-30383") @Test public void retryNodeBlockSynchAcrossRestarts() throws Throwable { logging.record(ExecutorStepExecution.class, Level.FINE).record(FlowExecutionList.class, Level.FINE); sessions.then(r -> { diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/ExecutorStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/ExecutorStepTest.java index ef08b7e6..179266c4 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/ExecutorStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/ExecutorStepTest.java @@ -1103,7 +1103,7 @@ private static List currentLabels(JenkinsRule r) { r.assertLogContains("hello", b); r.assertLogNotContains("world", b); r.assertLogContains("going offline", b); - r.assertLogContains("IOException: Unable to create live FilePath for " + agent.getNodeName(), b); + r.assertLogContains("AgentOfflineException: Unable to create live FilePath for " + agent.getNodeName(), b); }); }