From 4ad01c210f82419c8dc77ed0b70172c00abe966a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 25 Mar 2020 08:38:13 +0100 Subject: [PATCH] remove error prone InjectionContextRestartStrategy --- scm-webapp/pom.xml | 8 - .../InjectionContextRestartStrategy.java | 121 ----------- .../scm/lifecycle/RestartStrategyFactory.java | 21 +- .../classloading/ClassLoaderLifeCycle.java | 11 +- ...lassLoaderLifeCycleWithLeakPrevention.java | 189 ------------------ .../classloading/LoggingAdapter.java | 68 ------- .../InjectionContextRestartStrategyTest.java | 119 ----------- .../scm/lifecycle/RestartStrategyTest.java | 31 ++- .../ClassLoaderLifeCycleTest.java | 24 +-- ...LoaderLifeCycleWithLeakPreventionTest.java | 140 ------------- 10 files changed, 41 insertions(+), 691 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/LoggingAdapter.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 17025a6a46..24ba16333a 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -312,14 +312,6 @@ 1.23 - - - - se.jiderhamn.classloader-leak-prevention - classloader-leak-prevention-core - 2.7.0 - - diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java deleted file mode 100644 index 89c2334b69..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * 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 sonia.scm.lifecycle; - -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.event.RecreateEventBusEvent; -import sonia.scm.event.ScmEventBus; -import sonia.scm.event.ShutdownEventBusEvent; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * Restart strategy which tries to free, every resource used by the context, starts gc and re initializes the context. - * Warning: This strategy should only be used with an classloader lifecycle which protects the - * created plugin classloader from classloader leaks. - */ -class InjectionContextRestartStrategy implements RestartStrategy { - - static final String NAME = "context"; - - private static final String DISABLE_RESTART_PROPERTY = "sonia.scm.restart.disable"; - private static final String WAIT_PROPERTY = "sonia.scm.restart.wait"; - private static final String DISABLE_GC_PROPERTY = "sonia.scm.restart.disable-gc"; - - private static final AtomicLong INSTANCE_COUNTER = new AtomicLong(); - private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class); - - private boolean restartEnabled = !Boolean.getBoolean(DISABLE_RESTART_PROPERTY); - private long waitInMs = Integer.getInteger(WAIT_PROPERTY, 250); - private boolean gcEnabled = !Boolean.getBoolean(DISABLE_GC_PROPERTY); - - private final ClassLoader webAppClassLoader; - - InjectionContextRestartStrategy(ClassLoader webAppClassLoader) { - this.webAppClassLoader = webAppClassLoader; - } - - @VisibleForTesting - void setWaitInMs(long waitInMs) { - this.waitInMs = waitInMs; - } - - @VisibleForTesting - void setGcEnabled(boolean gcEnabled) { - this.gcEnabled = gcEnabled; - } - - @Override - public void restart(InjectionContext context) { - stop(context); - if (restartEnabled) { - start(context); - } else { - LOG.warn("restarting context is disabled"); - } - } - - @SuppressWarnings("squid:S1215") // suppress explicit gc call warning - private void start(InjectionContext context) { - LOG.debug("use WebAppClassLoader as ContextClassLoader, to avoid ClassLoader leaks"); - Thread.currentThread().setContextClassLoader(webAppClassLoader); - - LOG.warn("send recreate eventbus event"); - ScmEventBus.getInstance().post(new RecreateEventBusEvent()); - - // restart context delayed, to avoid timing problems - new Thread(() -> { - try { - if (gcEnabled){ - LOG.info("call gc to clean up memory from old instances"); - System.gc(); - } - - LOG.info("wait {}ms before re starting the context", waitInMs); - Thread.sleep(waitInMs); - - LOG.warn("reinitialize injection context"); - context.initialize(); - - LOG.debug("register injection context on new eventbus"); - ScmEventBus.getInstance().register(context); - } catch ( Exception ex) { - LOG.error("failed to restart", ex); - } - }, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start(); - } - - private void stop(InjectionContext context) { - LOG.warn("destroy injection context"); - context.destroy(); - - if (!restartEnabled) { - // shutdown eventbus, but do this only if restart is disabled - ScmEventBus.getInstance().post(new ShutdownEventBusEvent()); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java index e522f3ad41..4520bb3ddf 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java @@ -27,6 +27,8 @@ import com.google.common.base.Strings; import sonia.scm.PlatformType; import sonia.scm.util.SystemUtil; +import java.lang.reflect.Constructor; + final class RestartStrategyFactory { /** @@ -61,22 +63,29 @@ final class RestartStrategyFactory { return null; } else if (ExitRestartStrategy.NAME.equalsIgnoreCase(property)) { return new ExitRestartStrategy(); - } else if (InjectionContextRestartStrategy.NAME.equalsIgnoreCase(property)) { - return new InjectionContextRestartStrategy(webAppClassLoader); } else { - return fromClassName(property); + return fromClassName(property, webAppClassLoader); } } - private static RestartStrategy fromClassName(String property) { + private static RestartStrategy fromClassName(String className, ClassLoader classLoader) { try { - Class rsClass = Class.forName(property).asSubclass(RestartStrategy.class); - return rsClass.getConstructor().newInstance(); + Class rsClass = Class.forName(className).asSubclass(RestartStrategy.class); + return createInstance(rsClass, classLoader); } catch (Exception e) { throw new RestartNotSupportedException("failed to create restart strategy from property", e); } } + private static RestartStrategy createInstance(Class rsClass, ClassLoader classLoader) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException, NoSuchMethodException { + try { + Constructor constructor = rsClass.getConstructor(ClassLoader.class); + return constructor.newInstance(classLoader); + } catch (NoSuchMethodException ex) { + return rsClass.getConstructor().newInstance(); + } + } + private static RestartStrategy forPlatform() { // we do not use SystemUtil here, to allow testing String osName = System.getProperty(SystemUtil.PROPERTY_OSNAME); 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 5305193daf..e9c739e22f 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 @@ -21,10 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.lifecycle.classloading; -import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.lifecycle.LifeCycle; @@ -42,16 +41,8 @@ public abstract class ClassLoaderLifeCycle implements LifeCycle { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class); - @VisibleForTesting - static final String PROPERTY = "sonia.scm.lifecycle.classloading"; - public static ClassLoaderLifeCycle create() { ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); - String implementation = System.getProperty(PROPERTY); - if (ClassLoaderLifeCycleWithLeakPrevention.NAME.equalsIgnoreCase(implementation)) { - LOG.info("create new ClassLoaderLifeCycle with leak prevention"); - return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader); - } LOG.info("create new simple ClassLoaderLifeCycle"); return new SimpleClassLoaderLifeCycle(webappClassLoader); } 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 deleted file mode 100644 index f8bed61e53..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * 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 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 { - - public static final String NAME = "with-leak-prevention"; - - 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/LoggingAdapter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/LoggingAdapter.java deleted file mode 100644 index ec52672904..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/LoggingAdapter.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * 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 sonia.scm.lifecycle.classloading; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; - -/** - * Logging adapter for {@link ClassLoaderLeakPreventor}. - */ -public class LoggingAdapter implements se.jiderhamn.classloader.leak.prevention.Logger { - - @SuppressWarnings("squid:S3416") // suppress "loggers should be named for their enclosing classes" rule - private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLeakPreventor.class); - - @Override - public void debug(String msg) { - LOG.debug(msg); - } - - @Override - public void info(String msg) { - LOG.info(msg); - } - - @Override - public void warn(String msg) { - LOG.warn(msg); - } - - @Override - public void warn(Throwable t) { - LOG.warn(t.getMessage(), t); - } - - @Override - public void error(String msg) { - LOG.error(msg); - } - - @Override - public void error(Throwable t) { - LOG.error(t.getMessage(), t); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java deleted file mode 100644 index 100e0e9031..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * 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 sonia.scm.lifecycle; - -import com.github.legman.Subscribe; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.event.RecreateEventBusEvent; -import sonia.scm.event.ScmEventBus; - -import java.util.concurrent.TimeUnit; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -class InjectionContextRestartStrategyTest { - - @Mock - private RestartStrategy.InjectionContext context; - - private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(Thread.currentThread().getContextClassLoader()); - - @BeforeEach - void setWaitToZero() { - strategy.setWaitInMs(0L); - // disable gc during tests - strategy.setGcEnabled(false); - } - - @Test - void shouldCallDestroyAndInitialize() { - TestingInjectionContext ctx = new TestingInjectionContext(); - strategy.restart(ctx); - await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> assertThat(ctx.destroyed).isTrue()); - await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> assertThat(ctx.initialized).isTrue()); - } - - @Test - void shouldFireRecreateEventBusEvent() { - Listener listener = new Listener(); - ScmEventBus.getInstance().register(listener); - - strategy.restart(context); - - assertThat(listener.event).isNotNull(); - } - - @Test - void shouldRegisterContextAfterRestart() throws InterruptedException { - TestingInjectionContext ctx = new TestingInjectionContext(); - strategy.restart(ctx); - - await().atMost(1, TimeUnit.SECONDS).until(() -> ctx.initialized); - Thread.sleep(50L); - ScmEventBus.getInstance().post("hello event"); - - assertThat(ctx.event).isEqualTo("hello event"); - } - - public static class Listener { - - private RecreateEventBusEvent event; - - @Subscribe(async = false) - public void setEvent(RecreateEventBusEvent event) { - this.event = event; - } - } - - public static class TestingInjectionContext implements RestartStrategy.InjectionContext { - - private volatile String event; - private boolean initialized = false; - private boolean destroyed = false; - - @Subscribe(async = false) - public void setEvent(String event) { - this.event = event; - } - - @Override - public void initialize() { - this.initialized = true; - } - - @Override - public void destroy() { - this.destroyed = true; - } - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java index 3301e46151..f590281175 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java @@ -45,6 +45,16 @@ class RestartStrategyTest { }); } + @Test + void shouldReturnRestartStrategyFromSystemPropertyWithClassLoaderConstructor() { + withStrategy(ComplexRestartStrategy.class.getName(), (rs) -> { + assertThat(rs).containsInstanceOf(ComplexRestartStrategy.class) + .get() + .extracting("classLoader") + .isSameAs(classLoader); + }); + } + @Test void shouldThrowExceptionForNonStrategyClass() { withStrategy(RestartStrategyTest.class.getName(), () -> { @@ -74,13 +84,6 @@ class RestartStrategyTest { }); } - @Test - void shouldReturnInjectionContextRestartStrategy() { - withStrategy(InjectionContextRestartStrategy.NAME, (rs) -> { - assertThat(rs).containsInstanceOf(InjectionContextRestartStrategy.class); - }); - } - @ParameterizedTest @ValueSource(strings = { "linux", "darwin", "solaris", "freebsd", "openbsd" }) void shouldReturnPosixRestartStrategyForPosixBased(String os) { @@ -121,4 +124,18 @@ class RestartStrategyTest { } } + public static class ComplexRestartStrategy implements RestartStrategy { + + private final ClassLoader classLoader; + + public ComplexRestartStrategy(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void restart(InjectionContext context) { + + } + } + } 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 2855a6dde4..133bbcacbd 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 @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.lifecycle.classloading; import org.junit.jupiter.api.Test; @@ -30,28 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat; class ClassLoaderLifeCycleTest { - @Test - 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 shouldCreateWithLeakPreventionClassLoader() { - System.setProperty(ClassLoaderLifeCycle.PROPERTY, ClassLoaderLifeCycleWithLeakPrevention.NAME); - try { - ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); - assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class); - } finally { - System.clearProperty(ClassLoaderLifeCycle.PROPERTY); - } - } - @Test void shouldCreateDefaultClassLoader() { ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); 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 deleted file mode 100644 index f237ab8a30..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPreventionTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * 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 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); - } - -}