diff --git a/src/main/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecution.java b/src/main/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecution.java index acfc0d3..b5cfc85 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecution.java @@ -1,17 +1,20 @@ package org.jenkinsci.plugins.workflow.steps; +import hudson.ExtensionList; +import hudson.ExtensionPoint; import hudson.security.ACL; import hudson.security.ACLContext; import hudson.util.ClassLoaderSanityThreadFactory; import hudson.util.DaemonThreadFactory; import hudson.util.NamingThreadFactory; -import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import edu.umd.cs.findbugs.annotations.NonNull; import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; import org.springframework.security.core.Authentication; /** @@ -92,9 +95,26 @@ public void onResume() { static synchronized ExecutorService getExecutorService() { if (executorService == null) { - executorService = Executors.newCachedThreadPool(new NamingThreadFactory(new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()), "org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution")); + ExecutorService result = Executors.newCachedThreadPool(new NamingThreadFactory(new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()), "org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution")); + + for (ExecutorServiceAugmentor augmentor : ExtensionList.lookup(ExecutorServiceAugmentor.class)) { + result = augmentor.augment(result); + } + executorService = result; } return executorService; } + /** + * Extension point for augmenting the executorService of {@link SynchronousNonBlockingStepExecution}. + */ + @Restricted(Beta.class) + public interface ExecutorServiceAugmentor extends ExtensionPoint { + /** + * Augment the executor service used by {@link SynchronousNonBlockingStepExecution} and {@link GeneralNonBlockingStepExecution}. + * @param executorService the executor service to augment + */ + ExecutorService augment(ExecutorService executorService); + } + } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecutorServiceAugmentorTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecutorServiceAugmentorTest.java new file mode 100644 index 0000000..8a33c69 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/SynchronousNonBlockingStepExecutorServiceAugmentorTest.java @@ -0,0 +1,68 @@ +package org.jenkinsci.plugins.workflow.steps; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.hamcrest.Matchers; +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.LoggerRule; +import org.jvnet.hudson.test.TestExtension; + +public class SynchronousNonBlockingStepExecutorServiceAugmentorTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Rule + public LoggerRule loggerRule = + new LoggerRule().record(AugmentorTestExtension.class, Level.FINE).capture(10); + + /** + * As the JVM and classes are loaded only once for the whole test, {@link SynchronousNonBlockingStepExecution#getExecutorService()} augments only once. The current boolean keeps track of the augmentation status. + */ + private static boolean augmented = false; + + @Test + public void smokes_configured_once_A() throws Exception { + SynchronousNonBlockingStepExecution.getExecutorService(); + checkAugmentation(); + } + + @Test + public void smokes_configured_once_B() throws Exception { + SynchronousNonBlockingStepExecution.getExecutorService(); + checkAugmentation(); + } + + private void checkAugmentation() { + if (augmented) { + assertThat(loggerRule.getMessages(), Matchers.emptyIterable()); + } else { + assertThat(loggerRule.getMessages(), Matchers.hasItem("Augmenting")); + augmented = true; + } + } + + @AfterClass + public static void afterClass() { + // Reset the static state to ensure that the test can be run multiple times without issues. + assertThat(augmented, Matchers.is(true)); + } + + @TestExtension + public static class AugmentorTestExtension implements SynchronousNonBlockingStepExecution.ExecutorServiceAugmentor { + + private static final Logger LOGGER = Logger.getLogger(AugmentorTestExtension.class.getName()); + + @Override + public ExecutorService augment(ExecutorService executorService) { + LOGGER.fine(() -> "Augmenting"); + return executorService; + } + } +}