diff --git a/CHANGELOG.md b/CHANGELOG.md index aa519112bd..9a4de014bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - Support for Java versions > 8 +- Simple ClassLoaderLifeCycle to fix integration tests on Java > 8 ### Changed - Upgrade [Legman](https://github.com/sdorra/legman) to v1.6.2 in order to fix execution on Java versions > 8 diff --git a/scm-it/pom.xml b/scm-it/pom.xml index c38c73ce3e..9064a39fc1 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -200,6 +200,10 @@ java.awt.headless true + + sonia.scm.classloading.lifecycle + simple + /scm diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 487eb0bfeb..924e176024 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -720,6 +720,10 @@ scm.stage ${scm.stage} + + sonia.scm.classloading.lifecycle + simple + ${project.basedir}/src/main/conf/jetty.xml 0 @@ -805,6 +809,10 @@ scm.home target/scm-it + + sonia.scm.classloading.lifecycle + simple + ${project.basedir}/src/main/conf/jetty.xml 0 diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java index a64ed6fa43..fb7d991c1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java @@ -3,198 +3,75 @@ package sonia.scm.lifecycle.classloading; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; -import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp; -import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp; -import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator; -import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator; import sonia.scm.lifecycle.LifeCycle; import sonia.scm.plugin.ChildFirstPluginClassLoader; import sonia.scm.plugin.DefaultPluginClassLoader; -import java.io.Closeable; -import java.io.IOException; import java.net.URL; -import java.util.ArrayDeque; -import java.util.Deque; import static com.google.common.base.Preconditions.checkState; -import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT; /** - * Creates and shutdown SCM-Manager ClassLoaders. + * Base class for ClassLoader LifeCycle implementation in SCM-Manager. */ -public final class ClassLoaderLifeCycle implements LifeCycle { +public abstract class ClassLoaderLifeCycle implements LifeCycle { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class); - private Deque classLoaders = new ArrayDeque<>(); + @VisibleForTesting + static final String PROPERTY = "sonia.scm.classloading.lifecycle"; + + public static ClassLoaderLifeCycle create() { + ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); + String implementation = System.getProperty(PROPERTY); + if (SimpleClassLoaderLifeCycle.NAME.equalsIgnoreCase(implementation)) { + LOG.info("create new simple ClassLoaderLifeCycle"); + return new SimpleClassLoaderLifeCycle(webappClassLoader); + } + LOG.info("create new ClassLoaderLifeCycle with leak prevention"); + return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader); + } - private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; private final ClassLoader webappClassLoader; private BootstrapClassLoader bootstrapClassLoader; - private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() { - @Override - public C apply(C classLoader) { - return classLoader; - } - }; - - @VisibleForTesting - public static ClassLoaderLifeCycle create() { - ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); - ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = createClassLoaderLeakPreventorFactory(webappClassLoader); - return new ClassLoaderLifeCycle(webappClassLoader, classLoaderLeakPreventorFactory); - } - - ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) { - this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory; - this.webappClassLoader = initAndAppend(webappClassLoader); - } - - private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) { - // Should threads tied to the web app classloader be forced to stop at application shutdown? - boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads"); - - // Should Timer threads tied to the web app classloader be forced to stop at application shutdown? - boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads"); - - // Should shutdown hooks registered from the application be executed at application shutdown? - boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks"); - - // No of milliseconds to wait for threads to finish execution, before stopping them. - int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT); - - /* - * No of milliseconds to wait for shutdown hooks to finish execution, before stopping them. - * If set to -1 there will be no waiting at all, but Thread is allowed to run until finished. - */ - int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT); - - LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycle.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) ); - LOG.info(" stopThreads = {}", stopThreads); - LOG.info(" stopTimerThreads = {}", stopTimerThreads); - LOG.info(" executeShutdownHooks = {}", executeShutdownHooks); - LOG.info(" threadWaitMs = {} ms", threadWaitMs); - LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs); - - // use webapp classloader as safe base? or system? - ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader); - classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); - - final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class); - shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks); - shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs); - - final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class); - stopThreadsCleanUp.setStopThreads(stopThreads); - stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads); - stopThreadsCleanUp.setThreadWaitMs(threadWaitMs); - - // remove awt and imageio cleanup - classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class); - classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); - classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class); - classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class); - classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class); - - // the MBeanCleanUp causes a Exception and we use no mbeans - classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); - - return classLoaderLeakPreventorFactory; + ClassLoaderLifeCycle(ClassLoader webappClassLoader) { + this.webappClassLoader = webappClassLoader; } + @Override public void initialize() { bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader)); } - @VisibleForTesting - void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) { - this.classLoaderAppendListener = classLoaderAppendListener; - } + protected abstract T initAndAppend(T classLoader); public ClassLoader getBootstrapClassLoader() { checkState(bootstrapClassLoader != null, "%s was not initialized", ClassLoaderLifeCycle.class.getName()); return bootstrapClassLoader; } - public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) { - LOG.debug("create new PluginClassLoader for {}", plugin); - DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin); - return initAndAppend(pluginClassLoader); - } - public ClassLoader createChildFirstPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) { LOG.debug("create new ChildFirstPluginClassLoader for {}", plugin); ChildFirstPluginClassLoader pluginClassLoader = new ChildFirstPluginClassLoader(urls, parent, plugin); return initAndAppend(pluginClassLoader); } + public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) { + LOG.debug("create new PluginClassLoader for {}", plugin); + DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin); + return initAndAppend(pluginClassLoader); + } + + @Override public void shutdown() { LOG.info("shutdown classloader infrastructure"); - ClassLoaderAndPreventor clap = classLoaders.poll(); - while (clap != null) { - clap.shutdown(); - clap = classLoaders.poll(); - } - // be sure it is realy empty - classLoaders.clear(); - classLoaders = new ArrayDeque<>(); + shutdownClassLoaders(); bootstrapClassLoader.markAsShutdown(); bootstrapClassLoader = null; } - private T initAndAppend(T originalClassLoader) { - LOG.debug("init classloader {}", originalClassLoader); - T classLoader = classLoaderAppendListener.apply(originalClassLoader); - - ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader); - preventor.runPreClassLoaderInitiators(); - classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor)); - - return classLoader; - } - - interface ClassLoaderAppendListener { - C apply(C classLoader); - } - - private class ClassLoaderAndPreventor { - - private final ClassLoader classLoader; - private final ClassLoaderLeakPreventor preventor; - - private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) { - this.classLoader = classLoader; - this.preventor = preventor; - } - - void shutdown() { - LOG.debug("shutdown classloader {}", classLoader); - preventor.runCleanUps(); - - if (classLoader != webappClassLoader) { - close(); - } - } - - private void close() { - if (classLoader instanceof Closeable) { - LOG.trace("close classloader {}", classLoader); - try { - ((Closeable) classLoader).close(); - } catch (IOException e) { - LOG.warn("failed to close classloader", e); - } - } - } - } + protected abstract void shutdownClassLoaders(); } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java new file mode 100644 index 0000000000..e2e7a032b7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java @@ -0,0 +1,163 @@ +package sonia.scm.lifecycle.classloading; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; +import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp; +import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT; + +/** + * Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection. + */ +final class ClassLoaderLifeCycleWithLeakPrevention extends ClassLoaderLifeCycle { + + private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycleWithLeakPrevention.class); + + private Deque classLoaders = new ArrayDeque<>(); + + private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; + + private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return classLoader; + } + }; + + ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader) { + this(webappClassLoader, createClassLoaderLeakPreventorFactory(webappClassLoader)); + } + + ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) { + super(webappClassLoader); + this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory; + } + + private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) { + // Should threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads"); + + // Should Timer threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads"); + + // Should shutdown hooks registered from the application be executed at application shutdown? + boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks"); + + // No of milliseconds to wait for threads to finish execution, before stopping them. + int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT); + + /* + * No of milliseconds to wait for shutdown hooks to finish execution, before stopping them. + * If set to -1 there will be no waiting at all, but Thread is allowed to run until finished. + */ + int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT); + + LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycleWithLeakPrevention.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) ); + LOG.info(" stopThreads = {}", stopThreads); + LOG.info(" stopTimerThreads = {}", stopTimerThreads); + LOG.info(" executeShutdownHooks = {}", executeShutdownHooks); + LOG.info(" threadWaitMs = {} ms", threadWaitMs); + LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs); + + // use webapp classloader as safe base? or system? + ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader); + classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); + + final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class); + shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks); + shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs); + + final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class); + stopThreadsCleanUp.setStopThreads(stopThreads); + stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads); + stopThreadsCleanUp.setThreadWaitMs(threadWaitMs); + + // remove awt and imageio cleanup + classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); + classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class); + + // the MBeanCleanUp causes a Exception and we use no mbeans + classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); + + return classLoaderLeakPreventorFactory; + } + + @VisibleForTesting + void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) { + this.classLoaderAppendListener = classLoaderAppendListener; + } + + @Override + protected void shutdownClassLoaders() { + ClassLoaderAndPreventor clap = classLoaders.poll(); + while (clap != null) { + clap.shutdown(); + clap = classLoaders.poll(); + } + // be sure it is realy empty + classLoaders.clear(); + classLoaders = new ArrayDeque<>(); + } + + @Override + protected T initAndAppend(T originalClassLoader) { + LOG.debug("init classloader {}", originalClassLoader); + T classLoader = classLoaderAppendListener.apply(originalClassLoader); + + ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader); + preventor.runPreClassLoaderInitiators(); + classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor)); + + return classLoader; + } + + interface ClassLoaderAppendListener { + C apply(C classLoader); + } + + private static class ClassLoaderAndPreventor { + + private final ClassLoader classLoader; + private final ClassLoaderLeakPreventor preventor; + + private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) { + this.classLoader = classLoader; + this.preventor = preventor; + } + + void shutdown() { + LOG.debug("shutdown classloader {}", classLoader); + preventor.runCleanUps(); + close(); + } + + private void close() { + if (classLoader instanceof Closeable) { + LOG.trace("close classloader {}", classLoader); + try { + ((Closeable) classLoader).close(); + } catch (IOException e) { + LOG.warn("failed to close classloader", e); + } + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java new file mode 100644 index 0000000000..3c18c10680 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycle.java @@ -0,0 +1,56 @@ +package sonia.scm.lifecycle.classloading; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection. + */ +class SimpleClassLoaderLifeCycle extends ClassLoaderLifeCycle { + + static final String NAME = "simple"; + + private static final Logger LOG = LoggerFactory.getLogger(SimpleClassLoaderLifeCycle.class); + + private Deque classLoaders = new ArrayDeque<>(); + + SimpleClassLoaderLifeCycle(ClassLoader webappClassLoader) { + super(webappClassLoader); + } + + @Override + protected T initAndAppend(T classLoader) { + LOG.debug("init classloader {}", classLoader); + classLoaders.push(classLoader); + return classLoader; + } + + @Override + protected void shutdownClassLoaders() { + ClassLoader classLoader = classLoaders.poll(); + while (classLoader != null) { + shutdown(classLoader); + classLoader = classLoaders.poll(); + } + // be sure it is realy empty + classLoaders.clear(); + classLoaders = new ArrayDeque<>(); + } + + private void shutdown(ClassLoader classLoader) { + LOG.debug("shutdown classloader {}", classLoader); + if (classLoader instanceof Closeable) { + LOG.trace("close classloader {}", classLoader); + try { + ((Closeable) classLoader).close(); + } catch (IOException e) { + LOG.warn("failed to close classloader", e); + } + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java index a8f37777d7..619b09940e 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java @@ -1,116 +1,25 @@ package sonia.scm.lifecycle.classloading; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; - -import java.io.Closeable; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; -@ExtendWith(MockitoExtension.class) class ClassLoaderLifeCycleTest { - @Mock - private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; - - @Mock - private ClassLoaderLeakPreventor classLoaderLeakPreventor; - @Test - void shouldThrowIllegalStateExceptionWithoutInit() { - ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create(); - assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + void shouldCreateSimpleClassLoader() { + System.setProperty(ClassLoaderLifeCycle.PROPERTY, SimpleClassLoaderLifeCycle.NAME); + try { + ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); + assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class); + } finally { + System.clearProperty(ClassLoaderLifeCycle.PROPERTY); + } } @Test - void shouldThrowIllegalStateExceptionAfterShutdown() { - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(); - lifeCycle.initialize(); - - lifeCycle.shutdown(); - assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + void shouldCreateDefaultClassLoader() { + ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); + assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class); } - - @Test - void shouldCreateBootstrapClassLoaderOnInit() { - ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create(); - lifeCycle.initialize(); - - assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull(); - } - - @Test - void shouldCallTheLeakPreventor() { - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(); - - lifeCycle.initialize(); - verify(classLoaderLeakPreventor, times(2)).runPreClassLoaderInitiators(); - - lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); - lifeCycle.createPluginClassLoader(new URL[0], null, "b"); - verify(classLoaderLeakPreventor, times(4)).runPreClassLoaderInitiators(); - - lifeCycle.shutdown(); - verify(classLoaderLeakPreventor, times(4)).runCleanUps(); - } - - @Test - void shouldCloseCloseableClassLoaders() throws IOException { - // we use URLClassLoader, because we must be sure that the classloader is closable - URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); - - ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(webappClassLoader); - lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycle.ClassLoaderAppendListener() { - @Override - public C apply(C classLoader) { - return spy(classLoader); - } - }); - lifeCycle.initialize(); - - ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); - ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b"); - - lifeCycle.shutdown(); - - closed(pluginB); - closed(pluginA); - - neverClosed(webappClassLoader); - } - - private void neverClosed(Object object) throws IOException { - Closeable closeable = closeable(object); - verify(closeable, never()).close(); - } - - private void closed(Object object) throws IOException { - Closeable closeable = closeable(object); - verify(closeable).close(); - } - - private Closeable closeable(Object object) { - assertThat(object).isInstanceOf(Closeable.class); - return (Closeable) object; - } - - private ClassLoaderLifeCycle createMockedLifeCycle() { - return createMockedLifeCycle(Thread.currentThread().getContextClassLoader()); - } - - private ClassLoaderLifeCycle createMockedLifeCycle(ClassLoader classLoader) { - when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor); - return new ClassLoaderLifeCycle(classLoader, classLoaderLeakPreventorFactory); - } - } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java new file mode 100644 index 0000000000..9c76fbfd00 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java @@ -0,0 +1,116 @@ +package sonia.scm.lifecycle.classloading; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; +import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ClassLoaderLifeCycleWithLeakPreventionTest { + + @Mock + private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; + + @Mock + private ClassLoaderLeakPreventor classLoaderLeakPreventor; + + @Test + void shouldThrowIllegalStateExceptionWithoutInit() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader()); + assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + } + + @Test + void shouldThrowIllegalStateExceptionAfterShutdown() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(); + lifeCycle.initialize(); + + lifeCycle.shutdown(); + assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader); + } + + @Test + void shouldCreateBootstrapClassLoaderOnInit() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader()); + lifeCycle.initialize(); + + assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull(); + } + + @Test + void shouldCallTheLeakPreventor() { + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(); + + lifeCycle.initialize(); + verify(classLoaderLeakPreventor, times(1)).runPreClassLoaderInitiators(); + + lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); + lifeCycle.createPluginClassLoader(new URL[0], null, "b"); + verify(classLoaderLeakPreventor, times(3)).runPreClassLoaderInitiators(); + + lifeCycle.shutdown(); + verify(classLoaderLeakPreventor, times(3)).runCleanUps(); + } + + @Test + void shouldCloseCloseableClassLoaders() throws IOException { + // we use URLClassLoader, because we must be sure that the classloader is closable + URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); + + ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(webappClassLoader); + lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycleWithLeakPrevention.ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return spy(classLoader); + } + }); + lifeCycle.initialize(); + + ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a"); + ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b"); + + lifeCycle.shutdown(); + + closed(pluginB); + closed(pluginA); + + neverClosed(webappClassLoader); + } + + private void neverClosed(Object object) throws IOException { + Closeable closeable = closeable(object); + verify(closeable, never()).close(); + } + + private void closed(Object object) throws IOException { + Closeable closeable = closeable(object); + verify(closeable).close(); + } + + private Closeable closeable(Object object) { + assertThat(object).isInstanceOf(Closeable.class); + return (Closeable) object; + } + + private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle() { + return createMockedLifeCycle(Thread.currentThread().getContextClassLoader()); + } + + private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle(ClassLoader classLoader) { + when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor); + return new ClassLoaderLifeCycleWithLeakPrevention(classLoader, classLoaderLeakPreventorFactory); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java new file mode 100644 index 0000000000..a40d0fb353 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/SimpleClassLoaderLifeCycleTest.java @@ -0,0 +1,37 @@ +package sonia.scm.lifecycle.classloading; + +import org.junit.jupiter.api.Test; + +import java.io.Closeable; + +import static org.assertj.core.api.Assertions.assertThat; + +class SimpleClassLoaderLifeCycleTest { + + @Test + void shouldCloseClosableClassLoaderOnShutdown() { + SimpleClassLoaderLifeCycle lifeCycle = new SimpleClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader()); + lifeCycle.initialize(); + + ClosableClassLoader classLoader = new ClosableClassLoader(); + lifeCycle.initAndAppend(classLoader); + + lifeCycle.shutdown(); + + assertThat(classLoader.closed).isTrue(); + } + + private static class ClosableClassLoader extends ClassLoader implements Closeable { + + private boolean closed = false; + + public ClosableClassLoader() { + super(); + } + + @Override + public void close() { + closed = true; + } + } +}