diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 1759397d0b6d7..ad09c189edf78 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -63,6 +63,7 @@ import hudson.slaves.RetentionStrategy; import hudson.slaves.WorkspaceList; import hudson.triggers.SafeTimerTask; +import hudson.util.ClassLoaderSanityThreadFactory; import hudson.util.DaemonThreadFactory; import hudson.util.EditDistance; import hudson.util.ExceptionCatchingThreadFactory; @@ -1381,7 +1382,9 @@ public String call() throws IOException { Executors.newCachedThreadPool( new ExceptionCatchingThreadFactory( new NamingThreadFactory( - new DaemonThreadFactory(), "Computer.threadPoolForRemoting")))), ACL.SYSTEM2)); + new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()), + "Computer.threadPoolForRemoting")))), + ACL.SYSTEM2)); // // diff --git a/core/src/test/java/hudson/model/ComputerTest.java b/core/src/test/java/hudson/model/ComputerTest.java index d7c27880c1af8..bd5ddda4ab8c5 100644 --- a/core/src/test/java/hudson/model/ComputerTest.java +++ b/core/src/test/java/hudson/model/ComputerTest.java @@ -8,9 +8,11 @@ import hudson.FilePath; import hudson.security.ACL; import java.io.File; +import java.util.ArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import jenkins.model.Jenkins; +import jenkins.util.SetContextClassLoader; import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.springframework.security.core.Authentication; @@ -45,4 +47,42 @@ public void testThreadPoolForRemotingActsAsSystemUser() throws InterruptedExcept Future job = Computer.threadPoolForRemoting.submit(Jenkins::getAuthentication2); assertThat(job.get(), is(ACL.SYSTEM2)); } + + @Issue("JENKINS-72796") + @Test + public void testThreadPoolForRemotingContextClassLoaderIsSet() throws Exception { + // as the threadpool is cached, any other tests here pollute this test so we need enough threads to + // avoid any cached. + final int numThreads = 5; + + // simulate the first call to Computer.threadPoolForRemoting with a non default classloader + try (var ignored = new SetContextClassLoader(new ClassLoader() {})) { + obtainAndCheckThreadsContextClassloaderAreCorrect(numThreads); + } + // now repeat this as the checking that the pollution of the context classloader is handled + obtainAndCheckThreadsContextClassloaderAreCorrect(numThreads); + } + + private static void obtainAndCheckThreadsContextClassloaderAreCorrect(int numThreads) throws Exception { + ArrayList> classloaderFuturesList = new ArrayList<>(); + // block all calls to getContextClassloader() so we create more threads. + synchronized (WaitAndGetContextClassLoader.class) { + for (int i = 0; i < numThreads; i++) { + classloaderFuturesList.add(Computer.threadPoolForRemoting.submit(WaitAndGetContextClassLoader::getContextClassloader)); + } + } + for (Future fc : classloaderFuturesList) { + assertThat(fc.get(), is(Jenkins.class.getClassLoader())); + } + } + + private static class WaitAndGetContextClassLoader { + + public static synchronized ClassLoader getContextClassloader() throws InterruptedException { + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + // intentionally pollute the Threads context classloader + Thread.currentThread().setContextClassLoader(new ClassLoader() {}); + return ccl; + } + } }