diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsThreadGroup.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsThreadGroup.java index 19981832c..8e2946510 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsThreadGroup.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsThreadGroup.java @@ -24,35 +24,6 @@ package org.jenkinsci.plugins.workflow.cps; -import com.cloudbees.groovy.cps.Continuable; -import com.cloudbees.groovy.cps.Outcome; -import com.google.common.util.concurrent.Futures; -import com.thoughtworks.xstream.XStream; -import com.thoughtworks.xstream.converters.Converter; -import com.thoughtworks.xstream.converters.MarshallingContext; -import com.thoughtworks.xstream.converters.UnmarshallingContext; -import com.thoughtworks.xstream.io.HierarchicalStreamReader; -import com.thoughtworks.xstream.io.HierarchicalStreamWriter; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import groovy.lang.Closure; -import groovy.lang.GroovyShell; -import groovy.lang.Script; -import hudson.ExtensionList; -import hudson.Functions; -import hudson.Main; -import hudson.Util; -import hudson.model.Result; -import hudson.util.XStream2; -import jenkins.model.Jenkins; -import jenkins.util.Timer; -import org.jenkinsci.plugins.workflow.actions.ErrorAction; -import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn; -import org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext; -import org.jenkinsci.plugins.workflow.graph.FlowNode; -import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; -import org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter; - -import edu.umd.cs.findbugs.annotations.NonNull; import java.io.File; import java.io.IOException; import java.io.Serializable; @@ -60,7 +31,6 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -76,13 +46,46 @@ import java.util.logging.Level; import java.util.logging.Logger; -import edu.umd.cs.findbugs.annotations.CheckForNull; +import org.jenkinsci.plugins.workflow.actions.ErrorAction; +import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration; +import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn; +import org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext; +import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.pickles.Pickle; import org.jenkinsci.plugins.workflow.pickles.PickleFactory; +import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; import org.jenkinsci.plugins.workflow.support.concurrent.WithThreadName; import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory; +import org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter; import org.jenkinsci.plugins.workflow.support.storage.FlowNodeStorage; +import com.cloudbees.groovy.cps.Continuable; +import com.cloudbees.groovy.cps.Outcome; +import com.google.common.util.concurrent.Futures; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import groovy.lang.Closure; +import groovy.lang.GroovyShell; +import groovy.lang.Script; +import hudson.ExtensionList; +import hudson.Functions; +import hudson.Main; +import hudson.Util; +import hudson.model.Result; +import hudson.util.XStream2; +import jenkins.model.CauseOfInterruption; +import jenkins.model.Jenkins; +import jenkins.model.CauseOfInterruption.UserInterruption; +import jenkins.util.Timer; + /** * List of {@link CpsThread}s that form a single {@link CpsFlowExecution}. * @@ -303,10 +306,11 @@ public Void call() throws Exception { LOGGER.log(Level.WARNING, null, e); } } - if (paused.get() || j == null || (execution != null && j.isQuietingDown())) { + if (CPSConfiguration.get().isPipelinesPausingWhenQueitingDown() && (paused.get() || j == null || (execution != null && j.isQuietingDown()))) { if (j != null && j.isQuietingDown() && execution != null && pausedByQuietMode.compareAndSet(false, true)) { try { execution.getOwner().getListener().getLogger().println("Pausing (Preparing for shutdown)"); + //runner.awaitTermination(10, TimeUnit.SECONDS); } catch (IOException e) { LOGGER.log(Level.WARNING, null, e); } @@ -326,6 +330,35 @@ public void run() { saveProgramIfPossible(true); f.complete(null); return null; + } else { + // Do not pause build during quieting down period + + // Should we start a timer? + if (CPSConfiguration.get().isForcefullyStopBuldsAfterTimeout() ) { + + // Should likely not start multiple threads + Timer.get().schedule(new Runnable() { + @Override + public void run() { + if (j.isQuietingDown()) { + // still quieting down, let's stop this build + try { + execution.interrupt(Result.ABORTED, new ShutdownInterruption()); + } catch (IOException e) { + LOGGER.log(Level.WARNING, null, e); + } catch (InterruptedException interruptException) { + interruptException.printStackTrace(); + LOGGER.log(Level.WARNING, null, interruptException); + } + } else { + scheduleRun(); + } + } + }, Main.isUnitTest ? 1 : CPSConfiguration.get().getBuildTerminationTimeoutMinutes() * 60, + TimeUnit.SECONDS); + + } + } boolean stillRunnable = run(); diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/ShutdownInterruption.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/ShutdownInterruption.java new file mode 100644 index 000000000..34651d755 --- /dev/null +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/ShutdownInterruption.java @@ -0,0 +1,12 @@ +package org.jenkinsci.plugins.workflow.cps; + +import jenkins.model.CauseOfInterruption; + +public class ShutdownInterruption extends CauseOfInterruption { + + @Override + public String getShortDescription() { + return "Jenkins needs to terminate the execusion of this builds"; + } + +} diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java index 9de7f59c6..5708ab874 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java @@ -24,12 +24,13 @@ package org.jenkinsci.plugins.workflow.cps.config; +import org.jenkinsci.Symbol; + import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; -import org.jenkinsci.Symbol; @Symbol("cps") @Extension @@ -39,7 +40,10 @@ public class CPSConfiguration extends GlobalConfiguration { * Whether to show the sandbox checkbox in jobs to users without Jenkins.ADMINISTER */ private boolean hideSandbox; - + private boolean pipelinesPausingWhenQueitingDown = true; + private boolean forcefullyStopBuldsAfterTimeout = false; + private int buildTerminationTimeoutMinutes; + public CPSConfiguration() { load(); } @@ -52,6 +56,33 @@ public void setHideSandbox(boolean hideSandbox) { this.hideSandbox = hideSandbox; save(); } + + public boolean isPipelinesPausingWhenQueitingDown() { + return pipelinesPausingWhenQueitingDown; + } + + public void setPipelinesPausingWhenQueitingDown(boolean enabled) { + this.pipelinesPausingWhenQueitingDown = enabled; + save(); + } + + public boolean isForcefullyStopBuldsAfterTimeout() { + return forcefullyStopBuldsAfterTimeout; + } + + public void setForcefullyStopBuldsAfterTimeout(boolean stop) { + this.forcefullyStopBuldsAfterTimeout = stop; + save(); + } + + public int getBuildTerminationTimeoutMinutes() { + return buildTerminationTimeoutMinutes; + } + + public void setBuildTerminationTimeoutMinutes(int delay) { + this.buildTerminationTimeoutMinutes = delay; + save(); + } @NonNull @Override @@ -62,4 +93,5 @@ public GlobalConfigurationCategory getCategory() { public static CPSConfiguration get() { return ExtensionList.lookupSingleton(CPSConfiguration.class); } + } diff --git a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly index 0c7a1cebc..b9a740c8c 100644 --- a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly +++ b/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly @@ -24,10 +24,23 @@ THE SOFTWARE. --> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java index 184bb014e..4716350f2 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java @@ -89,6 +89,7 @@ import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.TimingFlowNodeStorage; import org.jenkinsci.plugins.workflow.cps.GroovySourceFileAllowlist.DefaultAllowlist; +import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.flow.FlowExecutionList; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; @@ -440,6 +441,37 @@ private static List stepNames(ListenableFuture> exec r.assertBuildStatusSuccess(r.waitForCompletion(b)); }); } + + @Test public void doNotPauseBuildsDuringQuietDown() throws Throwable { + sessions.then(r -> { + CPSConfiguration.get().setPipelinesPausingWhenQueitingDown(false); + WorkflowJob p = r.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("semaphore 'wait'; echo 'I am done'", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + r.jenkins.doQuietDown(true, 0, "Jenkins config change", false); + r.assertLogNotContains("Pausing (Preparing for shutdown)", b); + SemaphoreStep.success("wait/1", null); + r.waitForMessage("I am done", b); + }); + } + + @Test public void abortRunningBuildsDuringQuietDown() throws Throwable { + sessions.then(r -> { + CPSConfiguration.get().setPipelinesPausingWhenQueitingDown(false); + CPSConfiguration.get().setForcefullyStopBuldsAfterTimeout(true); + CPSConfiguration.get().setBuildTerminationTimeoutMinutes(1); // code overwrite this to 1 second + WorkflowJob p = r.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("semaphore 'wait'; echo 'I am done'", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + r.jenkins.doQuietDown(true, 0, "Jenkins config change", false); + r.assertLogNotContains("Pausing (Preparing for shutdown)", b); + SemaphoreStep.success("wait/1", null); + r.waitForMessage("Jenkins needs to terminate the execusion of this builds", b); + }); + } + public static final class SlowToResume extends Step { @DataBoundConstructor public SlowToResume() {} @Override public StepExecution start(StepContext context) throws Exception {