Commit 9ba78db8 authored by Andy Wilkinson's avatar Andy Wilkinson

Allow Undertow to stop when a request is being handled

Previously, unlike embedded Jetty, Netty, and Tomcat, Undertow would
not stop when one of its worker threads was in use. This meant that a
a long-running or stalled request could prevent the application from
shutting down in response to SIGTERM or SIGINT, and SIGTERM would be
required to get the process to exit.

This commit updates the factories for the reactive and servlet
Undertow web server factories to configure Undertow to use a 0ms
shutdown timeout. This aligns it with the behaviour of Jetty, Netty,
and Tomcat. Tests have been introduced to verify the behaviour across
the reactive and servlet variants of all four supported embedded web
servers.

Fixes gh-21319
parent 43e7ccd6
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -117,6 +117,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
else {
builder.addHttpListener(port, getListenAddress());
}
builder.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 0);
for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -241,6 +241,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
else {
builder.addHttpListener(port, getListenAddress());
}
builder.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 0);
for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
......
......@@ -23,7 +23,10 @@ import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
......@@ -333,6 +336,31 @@ public abstract class AbstractReactiveWebServerFactoryTests {
.hasMessageContaining("Could not load key store 'null'");
}
@Test
void whenARequestIsActiveThenStopWillComplete() throws InterruptedException, BrokenBarrierException {
AbstractReactiveWebServerFactory factory = getFactory();
CyclicBarrier barrier = new CyclicBarrier(2);
CountDownLatch latch = new CountDownLatch(1);
this.webServer = factory.getWebServer((request, response) -> {
try {
barrier.await();
latch.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
throw new IllegalStateException(ex);
}
return response.setComplete();
});
this.webServer.start();
new Thread(() -> getWebClient().build().get().uri("/").exchange().block()).start();
barrier.await();
this.webServer.stop();
latch.countDown();
}
protected WebClient prepareCompressionTest() {
Compression compression = new Compression();
compression.setEnabled(true);
......
......@@ -45,7 +45,10 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPInputStream;
......@@ -986,6 +989,43 @@ public abstract class AbstractServletWebServerFactoryTests {
.satisfies(this::wrapsFailingServletException);
}
@Test
void whenARequestIsActiveThenStopWillComplete() throws InterruptedException, BrokenBarrierException {
AbstractServletWebServerFactory factory = getFactory();
CyclicBarrier barrier = new CyclicBarrier(2);
CountDownLatch latch = new CountDownLatch(1);
this.webServer = factory.getWebServer((context) -> context.addServlet("blocking", new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try {
barrier.await();
latch.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
throw new ServletException(ex);
}
}
}).addMapping("/"));
this.webServer.start();
new Thread(() -> {
try {
getResponse(getLocalUrl("/"));
}
catch (Exception ex) {
// Continue
}
}).start();
barrier.await();
this.webServer.stop();
latch.countDown();
}
private void wrapsFailingServletException(WebServerException ex) {
Throwable cause = ex.getCause();
while (cause != null) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment