From 4823232ff0eacc21912c1563ee2fe6a94fedb217 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 23 Oct 2018 16:28:53 +0200 Subject: [PATCH] added RestartServlet, which is able to trigger a complete restart of the context --- .../main/java/sonia/scm/ResteasyModule.java | 20 +++ .../java/sonia/scm/ScmContextListener.java | 1 + .../scm/boot/BootstrapContextFilter.java | 43 +++--- .../scm/boot/BootstrapContextListener.java | 28 ---- .../java/sonia/scm/boot/RestartServlet.java | 97 +++++++++++++ .../sonia/scm/boot/ServletContextCleaner.java | 59 ++++++++ .../src/main/resources/logback.default.xml | 7 +- scm-webapp/src/main/webapp/WEB-INF/web.xml | 23 --- .../sonia/scm/boot/RestartServletTest.java | 133 ++++++++++++++++++ .../scm/boot/ServletContextCleanerTest.java | 53 +++++++ 10 files changed, 389 insertions(+), 75 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/ResteasyModule.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java new file mode 100644 index 0000000000..a85c3b6d06 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java @@ -0,0 +1,20 @@ +package sonia.scm; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.servlet.ServletModule; +import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher; +import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; + +import javax.inject.Singleton; +import java.util.Map; + +public class ResteasyModule extends ServletModule { + + @Override + protected void configureServlets() { + bind(HttpServletDispatcher.class).in(Singleton.class); + + Map initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api"); + serve("/api/*").with(HttpServletDispatcher.class, initParams); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 5a087a1c70..8ae005e826 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -126,6 +126,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); List moduleList = Lists.newArrayList(); + moduleList.add(new ResteasyModule()); moduleList.add(new ScmInitializerModule()); moduleList.add(new ScmEventBusModule()); moduleList.add(new EagerSingletonModule()); 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 bfa2218e76..aec8e2d653 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -34,22 +34,19 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- import com.github.legman.Subscribe; - import com.google.inject.servlet.GuiceFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.FilterConfig; import javax.servlet.ServletContextEvent; import javax.servlet.ServletException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -65,6 +62,8 @@ public class BootstrapContextFilter extends GuiceFilter //~--- methods -------------------------------------------------------------- + private final BootstrapContextListener listener = new BootstrapContextListener(); + /** * Restart the whole webapp context. * @@ -85,29 +84,20 @@ public class BootstrapContextFilter extends GuiceFilter } else { - - logger.warn( - "destroy filter pipeline, because of a received restart event"); + logger.warn("destroy filter pipeline, because of a received restart event"); destroy(); - logger.warn( - "reinitialize filter pipeline, because of a received restart event"); - super.init(filterConfig); + + logger.warn("reinitialize filter pipeline, because of a received restart event"); + initGuice(); } } - /** - * Method description - * - * - * @param filterConfig - * - * @throws ServletException - */ @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; - super.init(filterConfig); + + initGuice(); if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT) { @@ -116,6 +106,19 @@ public class BootstrapContextFilter extends GuiceFilter } } + public void initGuice() throws ServletException { + super.init(filterConfig); + + listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext())); + } + + @Override + public void destroy() { + super.destroy(); + listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext())); + ServletContextCleaner.cleanup(filterConfig.getServletContext()); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index bf7583b2a2..afaa28bfe8 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -187,40 +187,12 @@ public class BootstrapContextListener implements ServletContextListener return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction"); } - /** - * Restart the whole webapp context. - * - * - * @param event restart event - */ - @Subscribe - public void handleRestartEvent(RestartEvent event) - { - logger.warn("received restart event from {} with reason: {}", - event.getCause(), event.getReason()); - - if (context == null) - { - logger.error("context is null, scm-manager is not initialized"); - } - else - { - ServletContextEvent sce = new ServletContextEvent(context); - - logger.warn("destroy context, because of a received restart event"); - contextDestroyed(sce); - logger.warn("reinitialize context, because of a received restart event"); - contextInitialized(sce); - } - } - /** * Method description * * * @param context * @param pluginDirectory - * @param name * @param entry * * @throws IOException diff --git a/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java new file mode 100644 index 0000000000..c7177cc459 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java @@ -0,0 +1,97 @@ +package sonia.scm.boot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Priority; +import sonia.scm.SCMContext; +import sonia.scm.Stage; +import sonia.scm.event.ScmEventBus; +import sonia.scm.filter.WebElement; + +import javax.inject.Inject; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This servlet sends a {@link RestartEvent} to the {@link ScmEventBus} which causes scm-manager to restart the context. + * The {@link RestartServlet} can be used for reloading java code or for installing plugins without a complete restart. + * At the moment the Servlet accepts only request, if scm-manager was started in the {@link Stage#DEVELOPMENT} stage. + * + * @since 2.0.0 + */ +@Priority(0) +@WebElement("/restart") +public class RestartServlet extends HttpServlet { + + private static final Logger LOG = LoggerFactory.getLogger(RestartServlet.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final AtomicBoolean restarting = new AtomicBoolean(); + + private final ScmEventBus eventBus; + private final Stage stage; + + @Inject + public RestartServlet() { + this(ScmEventBus.getInstance(), SCMContext.getContext().getStage()); + } + + RestartServlet(ScmEventBus eventBus, Stage stage) { + this.eventBus = eventBus; + this.stage = stage; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + LOG.info("received sendRestartEvent request"); + + if (isRestartAllowed()) { + + try (InputStream requestInput = req.getInputStream()) { + Reason reason = objectMapper.readValue(requestInput, Reason.class); + sendRestartEvent(resp, reason); + } catch (IOException ex) { + LOG.warn("failed to trigger sendRestartEvent event", ex); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + } else { + LOG.debug("received restart event in non development stage"); + resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + } + + private boolean isRestartAllowed() { + return stage == Stage.DEVELOPMENT; + } + + private void sendRestartEvent(HttpServletResponse response, Reason reason) { + if ( restarting.compareAndSet(false, true) ) { + LOG.info("trigger sendRestartEvent, because of {}", reason.getMessage()); + eventBus.post(new RestartEvent(RestartServlet.class, reason.getMessage())); + + response.setStatus(HttpServletResponse.SC_ACCEPTED); + } else { + LOG.warn("scm-manager restarts already"); + response.setStatus(HttpServletResponse.SC_CONFLICT); + } + } + + public static class Reason { + + private String message; + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java new file mode 100644 index 0000000000..8b152ce329 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java @@ -0,0 +1,59 @@ +package sonia.scm.boot; + +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import java.util.Enumeration; +import java.util.Set; + +/** + * Remove cached resources from {@link ServletContext} to allow a clean restart of scm-manager without stale or + * duplicated data. + * + * @since 2.0.0 + */ +final class ServletContextCleaner { + + private static final Logger LOG = LoggerFactory.getLogger(ServletContextCleaner.class); + + private static final Set REMOVE_PREFIX = ImmutableSet.of( + "org.jboss.resteasy", + "resteasy", + "org.apache.shiro", + "sonia.scm" + ); + + private ServletContextCleaner() { + } + + /** + * Remove cached attributes from {@link ServletContext}. + * + * @param servletContext servlet context + */ + static void cleanup(ServletContext servletContext) { + LOG.info("remove cached attributes from context"); + + Enumeration attributeNames = servletContext.getAttributeNames(); + while( attributeNames.hasMoreElements()) { + String name = attributeNames.nextElement(); + if (shouldRemove(name)) { + LOG.info("remove attribute {} from servlet context", name); + servletContext.removeAttribute(name); + } else { + LOG.info("keep attribute {} in servlet context", name); + } + } + } + + private static boolean shouldRemove(String name) { + for (String prefix : REMOVE_PREFIX) { + if (name.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/scm-webapp/src/main/resources/logback.default.xml b/scm-webapp/src/main/resources/logback.default.xml index 33e914d04d..b12b9e0890 100644 --- a/scm-webapp/src/main/resources/logback.default.xml +++ b/scm-webapp/src/main/resources/logback.default.xml @@ -65,9 +65,6 @@ - - - @@ -93,7 +90,9 @@ --> - + + + diff --git a/scm-webapp/src/main/webapp/WEB-INF/web.xml b/scm-webapp/src/main/webapp/WEB-INF/web.xml index 8abba66aa3..e69eb9fb2a 100644 --- a/scm-webapp/src/main/webapp/WEB-INF/web.xml +++ b/scm-webapp/src/main/webapp/WEB-INF/web.xml @@ -41,10 +41,6 @@ - - sonia.scm.boot.BootstrapContextListener - - BootstrapFilter sonia.scm.boot.BootstrapContextFilter @@ -55,25 +51,6 @@ /* - - - - resteasy.servlet.mapping.prefix - /api - - - - Resteasy - - org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher - - - - - Resteasy - /api/* - -