diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java index 754359ad94..a963e95bb4 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -37,7 +37,6 @@ import com.github.legman.Subscribe; import com.google.inject.servlet.GuiceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.event.RecreateEventBusEvent; import sonia.scm.event.ScmEventBus; import javax.servlet.FilterConfig; @@ -50,63 +49,29 @@ import javax.servlet.ServletException; * * @author Sebastian Sdorra */ -public class BootstrapContextFilter extends GuiceFilter -{ +public class BootstrapContextFilter extends GuiceFilter { /** * the logger for BootstrapContextFilter */ - private static final Logger logger = - LoggerFactory.getLogger(BootstrapContextFilter.class); - - //~--- methods -------------------------------------------------------------- + private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextFilter.class); private final BootstrapContextListener listener = new BootstrapContextListener(); - /** - * Restart the whole webapp context. - * - * - * @param event restart event - * - * @throws ServletException - */ - @Subscribe - public void handleRestartEvent(RestartEvent event) throws ServletException - { - logger.warn("received restart event from {} with reason: {}", - event.getCause(), event.getReason()); - - if (filterConfig == null) - { - logger.error("filter config is null, scm-manager is not initialized"); - } - else - { - logger.warn("destroy filter pipeline, because of a received restart event"); - destroy(); - - logger.warn("send recreate eventbus event"); - ScmEventBus.getInstance().post(new RecreateEventBusEvent()); - ScmEventBus.getInstance().register(this); - - logger.warn("reinitialize filter pipeline, because of a received restart event"); - initGuice(); - } - } + /** Field description */ + private FilterConfig filterConfig; @Override - public void init(FilterConfig filterConfig) throws ServletException - { + public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; initGuice(); - logger.info("register for restart events"); + LOG.info("register for restart events"); ScmEventBus.getInstance().register(this); } - public void initGuice() throws ServletException { + private void initGuice() throws ServletException { super.init(filterConfig); listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext())); @@ -119,8 +84,39 @@ public class BootstrapContextFilter extends GuiceFilter ServletContextCleaner.cleanup(filterConfig.getServletContext()); } - //~--- fields --------------------------------------------------------------- + /** + * Restart SCM-Manager. + * + * @param event restart event + */ + @Subscribe + public void handleRestartEvent(RestartEvent event) { + LOG.warn("received restart event from {} with reason: {}", + event.getCause(), event.getReason()); + + if (filterConfig == null) { + LOG.error("filter config is null, scm-manager is not initialized"); + } else { + RestartStrategy restartStrategy = RestartStrategy.get(); + restartStrategy.restart(new GuiceInjectionContext()); + } + } + + private class GuiceInjectionContext implements RestartStrategy.InjectionContext { + + @Override + public void initialize() { + try { + BootstrapContextFilter.this.initGuice(); + } catch (ServletException e) { + throw new IllegalStateException("failed to initialize guice", e); + } + } + + @Override + public void destroy() { + BootstrapContextFilter.this.destroy(); + } + } - /** Field description */ - private FilterConfig filterConfig; } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/boot/InjectionContextRestartStrategy.java new file mode 100644 index 0000000000..29d59d733b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/InjectionContextRestartStrategy.java @@ -0,0 +1,50 @@ +package sonia.scm.boot; + +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 java.util.concurrent.atomic.AtomicLong; + +/** + * Restart strategy implementation which destroy the injection context and re initialize it. + */ +public class InjectionContextRestartStrategy implements RestartStrategy { + + private static final AtomicLong INSTANCE_COUNTER = new AtomicLong(); + private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class); + + private long waitInMs = 250L; + + @VisibleForTesting + void setWaitInMs(long waitInMs) { + this.waitInMs = waitInMs; + } + + @Override + public void restart(InjectionContext context) { + LOG.warn("destroy injection context"); + context.destroy(); + + LOG.warn("send recreate eventbus event"); + ScmEventBus.getInstance().post(new RecreateEventBusEvent()); + + // restart context delayed, to avoid timing problems + new Thread(() -> { + try { + Thread.sleep(waitInMs); + + LOG.warn("reinitialize injection context"); + context.initialize(); + + LOG.debug("re register injection context for events"); + ScmEventBus.getInstance().register(context); + } catch ( Exception ex) { + LOG.error("failed to restart", ex); + } + }, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start(); + + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/RestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/boot/RestartStrategy.java new file mode 100644 index 0000000000..fa1fd052c6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/RestartStrategy.java @@ -0,0 +1,38 @@ +package sonia.scm.boot; + +/** + * Strategy for restarting SCM-Manager. + */ +public interface RestartStrategy { + + /** + * Context for Injection in SCM-Manager. + */ + interface InjectionContext { + /** + * Initialize the injection context. + */ + void initialize(); + + /** + * Destroys the injection context. + */ + void destroy(); + } + + /** + * Restart SCM-Manager. + * @param context injection context + */ + void restart(InjectionContext context); + + /** + * Returns the configured strategy. + * + * @return configured strategy + */ + static RestartStrategy get() { + return new InjectionContextRestartStrategy(); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/InjectionContextRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/boot/InjectionContextRestartStrategyTest.java new file mode 100644 index 0000000000..985ab978b9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/InjectionContextRestartStrategyTest.java @@ -0,0 +1,90 @@ +package sonia.scm.boot; + +import com.github.legman.Subscribe; +import org.junit.jupiter.api.BeforeAll; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class InjectionContextRestartStrategyTest { + + @Mock + private RestartStrategy.InjectionContext context; + + private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(); + + @BeforeEach + void setWaitToZero() { + strategy.setWaitInMs(0L); + } + + @Test + void shouldCallDestroyAndInitialize() throws InterruptedException { + strategy.restart(context); + + verify(context).destroy(); + Thread.sleep(50L); + verify(context).initialize(); + } + + @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); + + 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; + + @Subscribe(async = false) + public void setEvent(String event) { + this.event = event; + } + + @Override + public void initialize() { + + } + + @Override + public void destroy() { + + } + } + +}