diff --git a/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java b/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java index 85f760a71f..52b80679d9 100644 --- a/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java +++ b/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java @@ -90,8 +90,12 @@ public class LegmanScmEventBus extends ScmEventBus @Override public void post(Object event) { - logger.debug("post {} to event bus {}", event, name); - eventBus.post(event); + if (eventBus != null) { + logger.debug("post {} to event bus {}", event, name); + eventBus.post(event); + } else { + logger.error("failed to post event {}, because event bus is shutdown", event); + } } /** @@ -104,9 +108,12 @@ public class LegmanScmEventBus extends ScmEventBus @Override public void register(Object object) { - logger.trace("register {} to event bus {}", object, name); - eventBus.register(object); - + if (eventBus != null) { + logger.trace("register {} to event bus {}", object, name); + eventBus.register(object); + } else { + logger.error("failed to register {}, because eventbus is shutdown", object); + } } /** @@ -118,22 +125,37 @@ public class LegmanScmEventBus extends ScmEventBus @Override public void unregister(Object object) { - logger.trace("unregister {} from event bus {}", object, name); - - try - { - eventBus.unregister(object); + if (eventBus != null) { + logger.trace("unregister {} from event bus {}", object, name); + + try { + eventBus.unregister(object); + } catch (IllegalArgumentException ex) { + logger.trace("object {} was not registered", object); + } + } else { + logger.error("failed to unregister object {}, because event bus is shutdown", object); } - catch (IllegalArgumentException ex) - { - logger.trace("object {} was not registered", object); + } + + @Subscribe(async = false) + public void shutdownEventBus(ShutdownEventBusEvent shutdownEventBusEvent) { + if (eventBus != null) { + logger.info("shutdown event bus executor for {}, because of received ShutdownEventBusEvent", name); + eventBus.shutdown(); + eventBus = null; + } else { + logger.warn("event bus was already shutdown"); } } @Subscribe(async = false) public void recreateEventBus(RecreateEventBusEvent recreateEventBusEvent) { - logger.info("shutdown event bus executor for {}", name); - eventBus.shutdown(); + if (eventBus != null) { + logger.info("shutdown event bus executor for {}, because of received RecreateEventBusEvent", name); + eventBus.shutdown(); + } + logger.info("recreate event bus because of received RecreateEventBusEvent"); eventBus = create(); } diff --git a/scm-webapp/src/main/java/sonia/scm/event/ShutdownEventBusEvent.java b/scm-webapp/src/main/java/sonia/scm/event/ShutdownEventBusEvent.java new file mode 100644 index 0000000000..5e5ab9ca5c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/event/ShutdownEventBusEvent.java @@ -0,0 +1,4 @@ +package sonia.scm.event; + +public class ShutdownEventBusEvent { +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java index ffb7631922..1d642d9c66 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java @@ -58,13 +58,16 @@ public class BootstrapContextFilter extends GuiceFilter { private final BootstrapContextListener listener = new BootstrapContextListener(); + private ClassLoader webAppClassLoader; + /** Field description */ private FilterConfig filterConfig; @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; - + // store webapp classloader for delayed restarts + webAppClassLoader = Thread.currentThread().getContextClassLoader(); initializeContext(); } @@ -97,7 +100,7 @@ public class BootstrapContextFilter extends GuiceFilter { if (filterConfig == null) { LOG.error("filter config is null, scm-manager is not initialized"); } else { - RestartStrategy restartStrategy = RestartStrategy.get(); + RestartStrategy restartStrategy = RestartStrategy.get(webAppClassLoader); restartStrategy.restart(new GuiceInjectionContext()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java index 683507c563..2db60580b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java @@ -5,6 +5,7 @@ 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; @@ -13,20 +14,47 @@ import java.util.concurrent.atomic.AtomicLong; */ public class InjectionContextRestartStrategy implements RestartStrategy { + 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 long waitInMs = 250L; + 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) { - LOG.warn("destroy injection context"); - context.destroy(); + 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()); @@ -34,6 +62,12 @@ public class InjectionContextRestartStrategy implements RestartStrategy { // 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"); @@ -45,6 +79,15 @@ public class InjectionContextRestartStrategy implements RestartStrategy { 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/RestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategy.java index 769351a850..6c7dd69259 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategy.java @@ -13,7 +13,6 @@ public interface RestartStrategy { * Initialize the injection context. */ void initialize(); - /** * Destroys the injection context. */ @@ -31,8 +30,8 @@ public interface RestartStrategy { * * @return configured strategy */ - static RestartStrategy get() { - return new InjectionContextRestartStrategy(); + static RestartStrategy get(ClassLoader webAppClassLoader) { + return new InjectionContextRestartStrategy(webAppClassLoader); } } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java index 355dca4a16..ddd691e5da 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/InjectionContextRestartStrategyTest.java @@ -18,11 +18,13 @@ class InjectionContextRestartStrategyTest { @Mock private RestartStrategy.InjectionContext context; - private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(); + private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(Thread.currentThread().getContextClassLoader()); @BeforeEach void setWaitToZero() { strategy.setWaitInMs(0L); + // disable gc during tests + strategy.setGcEnabled(false); } @Test @@ -47,7 +49,6 @@ class InjectionContextRestartStrategyTest { @Test void shouldRegisterContextAfterRestart() throws InterruptedException { TestingInjectionContext ctx = new TestingInjectionContext(); - strategy.restart(ctx); Thread.sleep(50L);