Remove WebSocketComponents & HouseKeeper on Server restart. (#6218)

* Remove WebSocketComponents & HouseKeeper on Server restart.
* Add testing for cleanup of websocket when stopping server.
* Add removeFilterHolder and removeFilterMapping methods on ServletHandler.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan 2021-05-20 22:48:23 +10:00 committed by GitHub
parent 802d32d2a8
commit cd73338b84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 372 additions and 68 deletions

View File

@ -339,6 +339,7 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi
_houseKeeper.stop();
if (_ownHouseKeeper)
{
removeBean(_houseKeeper);
_houseKeeper = null;
}
_random = null;

View File

@ -1195,6 +1195,34 @@ public class ServletHandler extends ScopedHandler
}
}
public void removeFilterHolder(FilterHolder holder)
{
if (holder == null)
return;
try (AutoLock ignored = lock())
{
FilterHolder[] holders = Arrays.stream(getFilters())
.filter(h -> h != holder)
.toArray(FilterHolder[]::new);
setFilters(holders);
}
}
public void removeFilterMapping(FilterMapping mapping)
{
if (mapping == null)
return;
try (AutoLock ignored = lock())
{
FilterMapping[] mappings = Arrays.stream(getFilterMappings())
.filter(m -> m != mapping)
.toArray(FilterMapping[]::new);
setFilterMappings(mappings);
}
}
protected void updateNameMappings()
{
try (AutoLock ignored = lock())

View File

@ -13,12 +13,12 @@
package org.eclipse.jetty.websocket.core.server;
import java.util.Objects;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.compression.DeflaterPool;
@ -93,17 +93,21 @@ public class WebSocketServerComponents extends WebSocketComponents
if (server.contains(bufferPool))
serverComponents.unmanage(bufferPool);
servletContext.setAttribute(WEBSOCKET_COMPONENTS_ATTRIBUTE, serverComponents);
LifeCycle.start(serverComponents);
servletContext.addListener(new ServletContextListener()
// Stop the WebSocketComponents when the ContextHandler stops.
ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext));
contextHandler.addManaged(serverComponents);
contextHandler.addEventListener(new LifeCycle.Listener()
{
@Override
public void contextDestroyed(ServletContextEvent sce)
public void lifeCycleStopping(LifeCycle event)
{
LifeCycle.stop(serverComponents);
servletContext.removeAttribute(WEBSOCKET_COMPONENTS_ATTRIBUTE);
contextHandler.removeBean(serverComponents);
contextHandler.removeEventListener(this);
}
});
servletContext.setAttribute(WEBSOCKET_COMPONENTS_ATTRIBUTE, serverComponents);
return serverComponents;
}

View File

@ -59,46 +59,60 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer
if (contextHandler.getServer() == null)
throw new IllegalStateException("Server has not been set on the ServletContextHandler");
JavaxWebSocketServerContainer container = getContainer(servletContext);
if (container == null)
JavaxWebSocketServerContainer containerFromServletContext = getContainer(servletContext);
if (containerFromServletContext != null)
return containerFromServletContext;
Function<WebSocketComponents, WebSocketCoreClient> coreClientSupplier = (wsComponents) ->
{
Function<WebSocketComponents, WebSocketCoreClient> coreClientSupplier = (wsComponents) ->
WebSocketCoreClient coreClient = (WebSocketCoreClient)servletContext.getAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE);
if (coreClient == null)
{
WebSocketCoreClient coreClient = (WebSocketCoreClient)servletContext.getAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE);
if (coreClient == null)
{
// Find Pre-Existing (Shared?) HttpClient and/or executor
HttpClient httpClient = (HttpClient)servletContext.getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE);
if (httpClient == null)
httpClient = (HttpClient)contextHandler.getServer().getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE);
// Find Pre-Existing (Shared?) HttpClient and/or executor
HttpClient httpClient = (HttpClient)servletContext.getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE);
if (httpClient == null)
httpClient = (HttpClient)contextHandler.getServer().getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE);
Executor executor = httpClient == null ? null : httpClient.getExecutor();
if (executor == null)
executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
executor = contextHandler.getServer().getThreadPool();
Executor executor = httpClient == null ? null : httpClient.getExecutor();
if (executor == null)
executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
executor = contextHandler.getServer().getThreadPool();
if (httpClient != null && httpClient.getExecutor() == null)
httpClient.setExecutor(executor);
if (httpClient != null && httpClient.getExecutor() == null)
httpClient.setExecutor(executor);
// create the core client
coreClient = new WebSocketCoreClient(httpClient, wsComponents);
coreClient.getHttpClient().setName("Javax-WebSocketClient@" + Integer.toHexString(coreClient.getHttpClient().hashCode()));
if (executor != null && httpClient == null)
coreClient.getHttpClient().setExecutor(executor);
servletContext.setAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE, coreClient);
}
return coreClient;
};
// create the core client
coreClient = new WebSocketCoreClient(httpClient, wsComponents);
coreClient.getHttpClient().setName("Javax-WebSocketClient@" + Integer.toHexString(coreClient.getHttpClient().hashCode()));
if (executor != null && httpClient == null)
coreClient.getHttpClient().setExecutor(executor);
servletContext.setAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE, coreClient);
}
return coreClient;
};
// Create the Jetty ServerContainer implementation
JavaxWebSocketServerContainer container = new JavaxWebSocketServerContainer(
WebSocketMappings.ensureMappings(servletContext),
WebSocketServerComponents.getWebSocketComponents(servletContext),
coreClientSupplier);
// Manage the lifecycle of the Container.
contextHandler.addManaged(container);
contextHandler.addEventListener(container);
contextHandler.addEventListener(new LifeCycle.Listener()
{
@Override
public void lifeCycleStopping(LifeCycle event)
{
servletContext.removeAttribute(JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE);
contextHandler.removeBean(container);
contextHandler.removeEventListener(container);
contextHandler.removeEventListener(this);
}
});
// Create the Jetty ServerContainer implementation
container = new JavaxWebSocketServerContainer(
WebSocketMappings.ensureMappings(servletContext),
WebSocketServerComponents.getWebSocketComponents(servletContext),
coreClientSupplier);
contextHandler.addManaged(container);
contextHandler.addEventListener(container);
}
// Store a reference to the ServerContainer per - javax.websocket spec 1.0 final - section 6.4: Programmatic Server Deployment
servletContext.setAttribute(JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE, container);
return container;
@ -140,18 +154,6 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer
this.frameHandlerFactory = new JavaxWebSocketServerFrameHandlerFactory(this);
}
@Override
public void lifeCycleStopping(LifeCycle context)
{
ContextHandler contextHandler = (ContextHandler)context;
JavaxWebSocketServerContainer container = contextHandler.getBean(JavaxWebSocketServerContainer.class);
if (container == this)
{
contextHandler.removeBean(container);
LifeCycle.stop(container);
}
}
@Override
public JavaxWebSocketServerFrameHandlerFactory getFrameHandlerFactory()
{

View File

@ -0,0 +1,120 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.javax.tests;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import javax.websocket.Session;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer;
import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class JavaxWebSocketRestartTest
{
private Server server;
private ServerConnector connector;
private JavaxWebSocketClientContainer client;
private ServletContextHandler contextHandler;
@BeforeEach
public void before() throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
server.setHandler(contextHandler);
client = new JavaxWebSocketClientContainer();
client.start();
}
@AfterEach
public void stop() throws Exception
{
client.stop();
server.stop();
}
@Test
public void testWebSocketRestart() throws Exception
{
JavaxWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
container.addEndpoint(EchoSocket.class));
server.start();
int numEventListeners = contextHandler.getEventListeners().size();
for (int i = 0; i < 100; i++)
{
server.stop();
server.start();
testEchoMessage();
}
// We have not accumulated websocket resources by restarting.
assertThat(contextHandler.getEventListeners().size(), is(numEventListeners));
assertThat(contextHandler.getContainedBeans(JavaxWebSocketServerContainer.class).size(), is(1));
assertThat(contextHandler.getContainedBeans(WebSocketServerComponents.class).size(), is(1));
assertNotNull(contextHandler.getServletContext().getAttribute(WebSocketServerComponents.WEBSOCKET_COMPONENTS_ATTRIBUTE));
assertNotNull(contextHandler.getServletContext().getAttribute(JavaxWebSocketServerContainer.JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE));
// We have one filter, and it is a WebSocketUpgradeFilter.
FilterHolder[] filters = contextHandler.getServletHandler().getFilters();
assertThat(filters.length, is(1));
assertThat(filters[0].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
// After stopping the websocket resources are cleaned up.
server.stop();
assertThat(contextHandler.getEventListeners().size(), is(0));
assertThat(contextHandler.getContainedBeans(JavaxWebSocketServerContainer.class).size(), is(0));
assertThat(contextHandler.getContainedBeans(WebSocketServerComponents.class).size(), is(0));
assertNull(contextHandler.getServletContext().getAttribute(WebSocketServerComponents.WEBSOCKET_COMPONENTS_ATTRIBUTE));
assertNull(contextHandler.getServletContext().getAttribute(JavaxWebSocketServerContainer.JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE));
assertThat(contextHandler.getServletHandler().getFilters().length, is(0));
}
private void testEchoMessage() throws Exception
{
// Test we can upgrade to websocket and send a message.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort());
EventSocket socket = new EventSocket();
try (Session session = client.connectToServer(socket, uri))
{
session.getBasicRemote().sendText("hello world");
}
assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS));
String msg = socket.textMessages.poll();
assertThat(msg, is("hello world"));
}
}

View File

@ -66,23 +66,37 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
if (contextHandler.getServer() == null)
throw new IllegalStateException("Server has not been set on the ServletContextHandler");
JettyWebSocketServerContainer container = getContainer(servletContext);
if (container == null)
// If we find a container in the servlet context return it.
JettyWebSocketServerContainer containerFromServletContext = getContainer(servletContext);
if (containerFromServletContext != null)
return containerFromServletContext;
// Find Pre-Existing executor.
Executor executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
executor = contextHandler.getServer().getThreadPool();
// Create the Jetty ServerContainer implementation.
WebSocketMappings mappings = WebSocketMappings.ensureMappings(servletContext);
WebSocketComponents components = WebSocketServerComponents.getWebSocketComponents(servletContext);
JettyWebSocketServerContainer container = new JettyWebSocketServerContainer(contextHandler, mappings, components, executor);
// Manage the lifecycle of the Container.
contextHandler.addManaged(container);
contextHandler.addEventListener(container);
contextHandler.addEventListener(new LifeCycle.Listener()
{
// Find Pre-Existing executor
Executor executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
executor = contextHandler.getServer().getThreadPool();
// Create the Jetty ServerContainer implementation
WebSocketMappings mappings = WebSocketMappings.ensureMappings(servletContext);
WebSocketComponents components = WebSocketServerComponents.getWebSocketComponents(servletContext);
container = new JettyWebSocketServerContainer(contextHandler, mappings, components, executor);
servletContext.setAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE, container);
contextHandler.addManaged(container);
contextHandler.addEventListener(container);
}
@Override
public void lifeCycleStopping(LifeCycle event)
{
contextHandler.getServletContext().removeAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE);
contextHandler.removeBean(container);
contextHandler.removeEventListener(container);
contextHandler.removeEventListener(this);
}
});
servletContext.setAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE, container);
return container;
}

View File

@ -0,0 +1,122 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.tests;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class JettyWebSocketRestartTest
{
private Server server;
private ServerConnector connector;
private WebSocketClient client;
private ServletContextHandler contextHandler;
@BeforeEach
public void before() throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
server.setHandler(contextHandler);
client = new WebSocketClient();
client.start();
}
@AfterEach
public void stop() throws Exception
{
client.stop();
server.stop();
}
@Test
public void testWebSocketRestart() throws Exception
{
JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
container.addMapping("/", EchoSocket.class));
server.start();
int numEventListeners = contextHandler.getEventListeners().size();
for (int i = 0; i < 100; i++)
{
server.stop();
server.start();
testEchoMessage();
}
// We have not accumulated websocket resources by restarting.
assertThat(contextHandler.getEventListeners().size(), is(numEventListeners));
assertThat(contextHandler.getContainedBeans(JettyWebSocketServerContainer.class).size(), is(1));
assertThat(contextHandler.getContainedBeans(WebSocketServerComponents.class).size(), is(1));
assertNotNull(contextHandler.getServletContext().getAttribute(WebSocketServerComponents.WEBSOCKET_COMPONENTS_ATTRIBUTE));
assertNotNull(contextHandler.getServletContext().getAttribute(JettyWebSocketServerContainer.JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE));
// We have one filter, and it is a WebSocketUpgradeFilter.
FilterHolder[] filters = contextHandler.getServletHandler().getFilters();
assertThat(filters.length, is(1));
assertThat(filters[0].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
// After stopping the websocket resources are cleaned up.
server.stop();
assertThat(contextHandler.getEventListeners().size(), is(0));
assertThat(contextHandler.getContainedBeans(JettyWebSocketServerContainer.class).size(), is(0));
assertThat(contextHandler.getContainedBeans(WebSocketServerComponents.class).size(), is(0));
assertNull(contextHandler.getServletContext().getAttribute(WebSocketServerComponents.WEBSOCKET_COMPONENTS_ATTRIBUTE));
assertNull(contextHandler.getServletContext().getAttribute(JettyWebSocketServerContainer.JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE));
assertThat(contextHandler.getServletHandler().getFilters().length, is(0));
}
private void testEchoMessage() throws Exception
{
// Test we can upgrade to websocket and send a message.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort());
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hello world");
}
assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS));
String msg = socket.textMessages.poll();
assertThat(msg, is("hello world"));
}
}

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.server.WebSocketMappings;
@ -123,6 +124,18 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
servletHandler.prependFilter(holder);
servletHandler.prependFilterMapping(mapping);
// If we create the filter we must also make sure it is removed if the context is stopped.
contextHandler.addEventListener(new LifeCycle.Listener()
{
@Override
public void lifeCycleStopping(LifeCycle event)
{
servletHandler.removeFilterHolder(holder);
servletHandler.removeFilterMapping(mapping);
contextHandler.removeEventListener(this);
}
});
if (LOG.isDebugEnabled())
LOG.debug("Adding {} mapped to {} in {}", holder, pathSpec, servletContext);
return holder;