added RestartServlet, which is able to trigger a complete restart of the context

This commit is contained in:
Sebastian Sdorra
2018-10-23 16:28:53 +02:00
parent f684a07404
commit 4823232ff0
10 changed files with 389 additions and 75 deletions

View File

@@ -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<String, String> initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api");
serve("/api/*").with(HttpServletDispatcher.class, initParams);
}
}

View File

@@ -126,6 +126,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
List<Module> moduleList = Lists.newArrayList();
moduleList.add(new ResteasyModule());
moduleList.add(new ScmInitializerModule());
moduleList.add(new ScmEventBusModule());
moduleList.add(new EagerSingletonModule());

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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<String> 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<String> 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;
}
}

View File

@@ -66,9 +66,6 @@
<logger name="sonia.scm.plugin.ext.DefaultAnnotationScanner" level="INFO" />
<logger name="sonia.scm.security.ConfigurableLoginAttemptHandler" level="DEBUG" />
<!-- event bus -->
<logger name="sonia.scm.event.LegmanScmEventBus" level="INFO" />
<!-- cgi -->
<logger name="sonia.scm.web.cgi.DefaultCGIExecutor" level="DEBUG" />
@@ -93,7 +90,9 @@
<logger name="net.sf.ehcache" level="DEBUG" />
-->
<logger name="org.jboss.resteasy" level="DEBUG" />
<logger name="org.jboss.resteasy" level="INFO" />
<logger name="sonia.scm.boot.RestartServlet" level="TRACE" />
<root level="WARN">
<appender-ref ref="STDOUT" />

View File

@@ -41,10 +41,6 @@
<!-- bootstraping -->
<listener>
<listener-class>sonia.scm.boot.BootstrapContextListener</listener-class>
</listener>
<filter>
<filter-name>BootstrapFilter</filter-name>
<filter-class>sonia.scm.boot.BootstrapContextFilter</filter-class>
@@ -55,25 +51,6 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- rest -->
<context-param>
<param-name>resteasy.servlet.mapping.prefix</param-name>
<param-value>/api</param-value>
</context-param>
<servlet>
<servlet-name>Resteasy</servlet-name>
<servlet-class>
org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Resteasy</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<!-- capture sessions -->
<!--
TODO remove, we need no longer a session

View File

@@ -0,0 +1,133 @@
package sonia.scm.boot;
import com.github.legman.Subscribe;
import com.google.common.base.Charsets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
import sonia.scm.event.ScmTestEventBus;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class RestartServletTest {
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private RestartServlet restartServlet;
private EventListener listener;
private void setUpObjectUnderTest(Stage stage) {
listener = new EventListener();
ScmEventBus eventBus = ScmTestEventBus.getInstance();
eventBus.register(listener);
restartServlet = new RestartServlet(eventBus, stage);
}
@Test
public void testRestart() throws IOException {
setUpObjectUnderTest(Stage.DEVELOPMENT);
setRequestInputReason("something changed");
restartServlet.doPost(request, response);
verify(response).setStatus(HttpServletResponse.SC_ACCEPTED);
RestartEvent restartEvent = listener.restartEvent;
assertThat(restartEvent).isNotNull();
assertThat(restartEvent.getCause()).isEqualTo(RestartServlet.class);
assertThat(restartEvent.getReason()).isEqualTo("something changed");
}
@Test
public void testRestartCalledTwice() throws IOException {
setUpObjectUnderTest(Stage.DEVELOPMENT);
setRequestInputReason("initial change");
restartServlet.doPost(request, response);
verify(response).setStatus(HttpServletResponse.SC_ACCEPTED);
setRequestInputReason("changed again");
restartServlet.doPost(request, response);
verify(response).setStatus(HttpServletResponse.SC_CONFLICT);
}
@Test
public void testRestartWithInvalidContent() throws IOException {
setUpObjectUnderTest(Stage.DEVELOPMENT);
setRequestInputContent("invalid json");
restartServlet.doPost(request, response);
verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
@Test
public void testRestartInProductionStage() throws IOException {
setUpObjectUnderTest(Stage.PRODUCTION);
setRequestInputReason("initial change");
restartServlet.doPost(request, response);
verify(response).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
private void setRequestInputReason(String message) throws IOException {
String content = createReason(message);
setRequestInputContent(content);
}
private void setRequestInputContent(String content) throws IOException {
InputStream input = createReasonAsInputStream(content);
when(request.getInputStream()).thenReturn(createServletInputStream(input));
}
private ServletInputStream createServletInputStream(final InputStream inputStream) {
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
private InputStream createReasonAsInputStream(String content) {
return new ByteArrayInputStream(content.getBytes(Charsets.UTF_8));
}
private String createReason(String message) {
return String.format("{\"message\": \"%s\"}", message);
}
public static class EventListener {
private RestartEvent restartEvent;
@Subscribe(async = false)
public void store(RestartEvent restartEvent) {
this.restartEvent = restartEvent;
}
}
}

View File

@@ -0,0 +1,53 @@
package sonia.scm.boot;
import com.google.common.collect.ImmutableSet;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.ServletContext;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Set;
import java.util.Vector;
import static org.junit.Assert.*;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ServletContextCleanerTest {
@Mock
private ServletContext servletContext;
@Test
public void testCleanup() {
Set<String> names = ImmutableSet.of(
"org.jboss.resteasy.Dispatcher",
"resteasy.Deployment",
"sonia.scm.Context",
"org.eclipse.jetty.HttpServer",
"javax.servlet.Context",
"org.apache.shiro.SecurityManager"
);
when(servletContext.getAttributeNames()).thenReturn(toEnumeration(names));
ServletContextCleaner.cleanup(servletContext);
verify(servletContext).removeAttribute("org.jboss.resteasy.Dispatcher");
verify(servletContext).removeAttribute("resteasy.Deployment");
verify(servletContext).removeAttribute("sonia.scm.Context");
verify(servletContext, never()).removeAttribute("org.eclipse.jetty.HttpServer");
verify(servletContext, never()).removeAttribute("javax.servlet.Context");
verify(servletContext).removeAttribute("org.apache.shiro.SecurityManager");
}
private <T> Enumeration<T> toEnumeration(Collection<T> collection) {
return new Vector<>(collection).elements();
}
}