Jetty 12 graceful contexts (#9867)

Removed all shutdown mechanisms from ContextHandler
Fixed GracefulHandler

---------

Signed-off-by: gregw <gregw@webtide.com>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Co-authored-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Greg Wilkins 2023-06-07 21:05:49 +02:00 committed by GitHub
parent 18a0738923
commit c0de62b2c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 336 additions and 165 deletions

View File

@ -25,7 +25,6 @@ import java.util.EventListener;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@ -54,7 +53,6 @@ import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.component.ClassLoaderDump;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
@ -63,14 +61,8 @@ import org.eclipse.jetty.util.thread.Invocable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ContextHandler extends Handler.Wrapper implements Attributes, Graceful, AliasCheck
public class ContextHandler extends Handler.Wrapper implements Attributes, AliasCheck
{
// TODO where should the alias checking go?
// TODO add protected paths to ServletContextHandler?
// TODO what about ObjectFactory stuff
// TODO what about a Context logger?
// TODO init param stuff to ServletContextHandler
private static final Logger LOG = LoggerFactory.getLogger(ContextHandler.class);
private static final ThreadLocal<Context> __context = new ThreadLocal<>();
@ -147,10 +139,9 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
public enum Availability
{
STOPPED, // stopped and can't be made unavailable nor shutdown
STARTING, // starting inside of doStart. It may go to any of the next states.
STARTING, // starting inside doStart. It may go to any of the next states.
AVAILABLE, // running normally
UNAVAILABLE, // Either a startup error or explicit call to setAvailable(false)
SHUTDOWN, // graceful shutdown
}
/**
@ -583,29 +574,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
}
}
/**
* @return true if this context is shutting down
*/
@ManagedAttribute("true for graceful shutdown, which allows existing requests to complete")
public boolean isShutdown()
{
// TODO
return false;
}
/**
* Set shutdown status. This field allows for graceful shutdown of a context. A started context may be put into non accepting state so that existing
* requests can complete, but no new requests are accepted.
*/
@Override
public CompletableFuture<Void> shutdown()
{
// TODO
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
completableFuture.complete(null);
return completableFuture;
}
/**
* @return false if this context is unavailable (sends 503)
*/
@ -651,15 +619,16 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
Availability availability = _availability.get();
switch (availability)
{
case STARTING:
case AVAILABLE:
if (!_availability.compareAndSet(availability, Availability.UNAVAILABLE))
continue;
break;
default:
break;
case STARTING, AVAILABLE ->
{
if (_availability.compareAndSet(availability, Availability.UNAVAILABLE))
return;
}
default ->
{
return;
}
}
break;
}
}
}
@ -1160,14 +1129,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
return (H)ContextHandler.this;
}
@Override
public Object getAttribute(String name)
{
// TODO the Attributes.Layer is a little different to previous
// behaviour. We need to verify if that is OK
return super.getAttribute(name);
}
@Override
public Request.Handler getErrorHandler()
{

View File

@ -21,6 +21,8 @@ import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.component.Graceful;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,17 +34,17 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
{
private static final Logger LOG = LoggerFactory.getLogger(GracefulHandler.class);
private final LongAdder dispatchedStats = new LongAdder();
private final Shutdown shutdown;
private final LongAdder _requests = new LongAdder();
private final Shutdown _shutdown;
public GracefulHandler()
{
shutdown = new Shutdown(this)
_shutdown = new Shutdown(this)
{
@Override
public boolean isShutdownDone()
{
long count = dispatchedStats.sum();
long count = getCurrentRequestCount();
if (LOG.isDebugEnabled())
LOG.debug("isShutdownDone: count {}", count);
return count == 0;
@ -50,6 +52,12 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
};
}
@ManagedAttribute("number of requests being currently handled")
public long getCurrentRequestCount()
{
return _requests.sum();
}
/**
* Flag indicating that Graceful shutdown has been initiated.
*
@ -59,7 +67,7 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
@Override
public boolean isShutdown()
{
return shutdown.isShutdown();
return _shutdown.isShutdown();
}
@Override
@ -86,18 +94,18 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
{
boolean handled = super.handle(request, response, shutdownCallback);
if (!handled)
shutdownCallback.decrement();
shutdownCallback.completed();
return handled;
}
catch (Throwable t)
{
shutdownCallback.decrement();
throw t;
Response.writeError(request, response, shutdownCallback, t);
return true;
}
finally
{
if (isShutdown())
shutdown.check();
_shutdown.check();
}
}
@ -106,43 +114,28 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
{
if (LOG.isDebugEnabled())
LOG.debug("Shutdown requested");
return shutdown.shutdown();
return _shutdown.shutdown();
}
private class ShutdownTrackingCallback extends Callback.Nested
private class ShutdownTrackingCallback extends CountingCallback
{
final Request request;
final Response response;
public ShutdownTrackingCallback(Request request, Response response, Callback callback)
{
super(callback);
super(callback, 1);
this.request = request;
this.response = response;
dispatchedStats.increment();
}
public void decrement()
{
dispatchedStats.decrement();
_requests.increment();
}
@Override
public void failed(Throwable x)
public void completed()
{
decrement();
super.failed(x);
_requests.decrement();
if (isShutdown())
shutdown.check();
}
@Override
public void succeeded()
{
decrement();
super.succeeded();
if (isShutdown())
shutdown.check();
_shutdown.check();
}
}
}

View File

@ -52,12 +52,11 @@ public class GracefulHandlerTest
{
private static final Logger LOG = LoggerFactory.getLogger(GracefulHandlerTest.class);
private Server server;
private ServerConnector connector;
public Server createServer(Handler handler) throws Exception
{
server = new Server();
connector = new ServerConnector(server, 1, 1);
ServerConnector connector = new ServerConnector(server, 1, 1);
connector.setIdleTimeout(10000);
connector.setShutdownIdleTimeout(1000);
connector.setPort(0);

View File

@ -19,20 +19,27 @@ import java.io.Writer;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.awaitility.Awaitility;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Connector;
@ -40,6 +47,7 @@ import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.MockConnectionMetaData;
import org.eclipse.jetty.server.MockConnector;
import org.eclipse.jetty.server.MockHttpStream;
@ -52,6 +60,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.Graceful;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -891,4 +900,165 @@ public class ContextHandlerTest
assertThat(r, sameInstance(request));
}
}
@Test
public void testGraceful() throws Exception
{
// This is really just another test of GracefulHandler, but good to check it works inside of ContextHandler
CountDownLatch latch0 = new CountDownLatch(1);
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch requests = new CountDownLatch(7);
Handler handler = new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
requests.countDown();
switch (request.getContext().getPathInContext(request.getHttpURI().getCanonicalPath()))
{
case "/ignore0" ->
{
try
{
latch0.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
return false;
}
case "/ignore1" ->
{
try
{
latch1.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
return false;
}
case "/ok0" ->
{
try
{
latch0.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
case "/ok1" ->
{
try
{
latch1.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
case "/fail0" ->
{
try
{
latch0.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
throw new QuietException.Exception("expected0");
}
case "/fail1" ->
{
try
{
latch1.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
callback.failed(new QuietException.Exception("expected1"));
}
default ->
{
}
}
response.setStatus(HttpStatus.OK_200);
callback.succeeded();
return true;
}
};
_contextHandler.setHandler(handler);
GracefulHandler gracefulHandler = new GracefulHandler();
_contextHandler.insertHandler(gracefulHandler);
LocalConnector connector = new LocalConnector(_server);
_server.addConnector(connector);
_server.start();
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse("GET /ctx/ HTTP/1.0\r\n\r\n"));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
List<LocalConnector.LocalEndPoint> endPoints = new ArrayList<>();
for (String target : new String[] {"/ignore", "/ok", "/fail"})
{
for (int batch = 0; batch <= 1; batch++)
{
LocalConnector.LocalEndPoint endPoint = connector.executeRequest("GET /ctx%s%d HTTP/1.0\r\n\r\n".formatted(target, batch));
endPoints.add(endPoint);
}
}
assertTrue(requests.await(10, TimeUnit.SECONDS));
assertThat(gracefulHandler.getCurrentRequestCount(), is(6L));
CompletableFuture<Void> shutdown = Graceful.shutdown(_contextHandler);
assertFalse(shutdown.isDone());
assertThat(gracefulHandler.getCurrentRequestCount(), is(6L));
response = HttpTester.parseResponse(connector.getResponse("GET /ctx/ HTTP/1.0\r\n\r\n"));
assertThat(response.getStatus(), is(HttpStatus.SERVICE_UNAVAILABLE_503));
latch0.countDown();
response = HttpTester.parseResponse(endPoints.get(0).getResponse());
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
response = HttpTester.parseResponse(endPoints.get(2).getResponse());
assertThat(response.getStatus(), is(HttpStatus.OK_200));
response = HttpTester.parseResponse(endPoints.get(4).getResponse());
assertThat(response.getStatus(), is(HttpStatus.INTERNAL_SERVER_ERROR_500));
assertFalse(shutdown.isDone());
Awaitility.waitAtMost(10, TimeUnit.SECONDS).until(() -> gracefulHandler.getCurrentRequestCount() == 3L);
assertThat(gracefulHandler.getCurrentRequestCount(), is(3L));
latch1.countDown();
response = HttpTester.parseResponse(endPoints.get(1).getResponse());
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
response = HttpTester.parseResponse(endPoints.get(3).getResponse());
assertThat(response.getStatus(), is(HttpStatus.OK_200));
response = HttpTester.parseResponse(endPoints.get(5).getResponse());
assertThat(response.getStatus(), is(HttpStatus.INTERNAL_SERVER_ERROR_500));
shutdown.get(10, TimeUnit.SECONDS);
assertTrue(shutdown.isDone());
assertThat(gracefulHandler.getCurrentRequestCount(), is(0L));
}
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.util;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@ -172,6 +173,12 @@ public interface Callback extends Invocable
{
return invocationType;
}
@Override
public String toString()
{
return "Callback@%x{%s, %s,%s}".formatted(hashCode(), invocationType, success, failure);
}
};
}
@ -183,13 +190,7 @@ public interface Callback extends Invocable
*/
static Callback from(Runnable completed)
{
return new Completing()
{
public void completed()
{
completed.run();
}
};
return from(Invocable.getInvocationType(completed), completed);
}
/**
@ -202,13 +203,25 @@ public interface Callback extends Invocable
*/
static Callback from(InvocationType invocationType, Runnable completed)
{
return new Completing(invocationType)
return new Completing()
{
@Override
public void completed()
{
completed.run();
}
@Override
public InvocationType getInvocationType()
{
return invocationType;
}
@Override
public String toString()
{
return "Callback.Completing@%x{%s,%s}".formatted(hashCode(), invocationType, completed);
}
};
}
@ -309,81 +322,40 @@ public interface Callback extends Invocable
*/
static Callback from(Callback callback1, Callback callback2)
{
return new Callback()
{
@Override
public void succeeded()
{
callback1.succeeded();
callback2.succeeded();
}
@Override
public void failed(Throwable x)
{
callback1.failed(x);
callback2.failed(x);
}
};
return combine(callback1, callback2);
}
/**
* <p>A Callback implementation that calls the {@link #completed()} method when it either succeeds or fails.</p>
*/
class Completing implements Callback
interface Completing extends Callback
{
private final InvocationType invocationType;
public Completing()
{
this(InvocationType.BLOCKING);
}
public Completing(InvocationType invocationType)
{
this.invocationType = invocationType;
}
void completed();
@Override
public void succeeded()
default void succeeded()
{
completed();
}
@Override
public void failed(Throwable x)
default void failed(Throwable x)
{
completed();
}
@Override
public InvocationType getInvocationType()
{
return invocationType;
}
public void completed()
{
}
}
/**
* Nested Completing Callback that completes after
* completing the nested callback
*/
class Nested extends Completing
class Nested implements Completing
{
private final Callback callback;
public Nested(Callback callback)
{
super(Invocable.getInvocationType(callback));
this.callback = callback;
}
public Nested(Nested nested)
{
this(nested.callback);
this.callback = Objects.requireNonNull(callback);
}
public Callback getCallback()
@ -391,6 +363,11 @@ public interface Callback extends Invocable
return callback;
}
@Override
public void completed()
{
}
@Override
public void succeeded()
{
@ -461,7 +438,7 @@ public interface Callback extends Invocable
}
catch (Throwable t)
{
if (x != t)
if (ExceptionUtil.areNotAssociated(x, t))
x.addSuppressed(t);
}
finally

View File

@ -108,6 +108,11 @@ public interface Graceful
done.complete(null);
}
/**
* This method can be called after {@link #shutdown()} has been called, but before
* {@link #check()} has been called with {@link #isShutdownDone()} having returned
* true to cancel the effects of the {@link #shutdown()} call.
*/
public void cancel()
{
CompletableFuture<Void> done = _done.get();

View File

@ -0,0 +1,88 @@
//
// ========================================================================
// Copyright (c) 1995 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.util.component;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class GracefulShutdownTest
{
@Test
public void testGracefulShutdown() throws Exception
{
AtomicBoolean isShutdown = new AtomicBoolean();
Graceful.Shutdown shutdown = new Graceful.Shutdown("testGracefulShutdown")
{
@Override
public boolean isShutdownDone()
{
return isShutdown.get();
}
};
assertThat(shutdown.isShutdown(), is(false));
shutdown.check();
assertThat(shutdown.isShutdown(), is(false));
CompletableFuture<Void> cf = shutdown.shutdown();
assertThat(shutdown.isShutdown(), is(true));
shutdown.check();
assertThat(shutdown.isShutdown(), is(true));
assertThrows(TimeoutException.class, () -> cf.get(10, TimeUnit.MILLISECONDS));
isShutdown.set(true);
shutdown.check();
assertThat(shutdown.isShutdown(), is(true));
assertThat(cf.get(), nullValue());
}
@Test
public void testGracefulShutdownCancel() throws Exception
{
AtomicBoolean isShutdown = new AtomicBoolean();
Graceful.Shutdown shutdown = new Graceful.Shutdown("testGracefulShutdownCancel")
{
@Override
public boolean isShutdownDone()
{
return isShutdown.get();
}
};
CompletableFuture<Void> cf1 = shutdown.shutdown();
shutdown.cancel();
assertThat(shutdown.isShutdown(), is(false));
assertThrows(CancellationException.class, cf1::get);
CompletableFuture<Void> cf2 = shutdown.shutdown();
assertThat(shutdown.isShutdown(), is(true));
isShutdown.set(true);
assertThrows(TimeoutException.class, () -> cf2.get(10, TimeUnit.MILLISECONDS));
shutdown.check();
assertThat(shutdown.isShutdown(), is(true));
assertThat(cf2.get(), nullValue());
}
}

View File

@ -95,7 +95,6 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.component.Environment;
import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
@ -120,7 +119,7 @@ import static jakarta.servlet.ServletContext.TEMPDIR;
* cause confusion with {@link ServletContext}.
*/
@ManagedObject("Servlet Context Handler")
public class ServletContextHandler extends ContextHandler implements Graceful
public class ServletContextHandler extends ContextHandler
{
private static final Logger LOG = LoggerFactory.getLogger(ServletContextHandler.class);
public static final Environment __environment = Environment.ensure("ee10");

View File

@ -31,7 +31,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
@ -90,7 +89,6 @@ import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.component.Environment;
import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.resource.Resources;
@ -121,7 +119,7 @@ import org.slf4j.LoggerFactory;
* </p>
*/
@ManagedObject("EE9 Context")
public class ContextHandler extends ScopedHandler implements Attributes, Graceful, Supplier<Handler>
public class ContextHandler extends ScopedHandler implements Attributes, Supplier<Handler>
{
public static final Environment ENVIRONMENT = Environment.ensure("ee9");
public static final int SERVLET_MAJOR_VERSION = 5;
@ -552,25 +550,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return getEventListeners().contains(listener);
}
/**
* @return true if this context is shutting down
*/
@ManagedAttribute("true for graceful shutdown, which allows existing requests to complete")
public boolean isShutdown()
{
return _coreContextHandler.isShutdown();
}
/**
* Set shutdown status. This field allows for graceful shutdown of a context. A started context may be put into non accepting state so that existing
* requests can complete, but no new requests are accepted.
*/
@Override
public CompletableFuture<Void> shutdown()
{
return _coreContextHandler.shutdown();
}
/**
* @return false if this context is unavailable (sends 503)
*/

View File

@ -1530,7 +1530,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
if (x instanceof HttpException httpException)
{
MetaData.Response responseMeta = new MetaData.Response(httpException.getCode(), httpException.getReason(), HttpVersion.HTTP_1_1, HttpFields.build().add(HttpFields.CONNECTION_CLOSE), 0);
send(_request.getMetaData(), responseMeta, null, true, new Nested(this)
send(_request.getMetaData(), responseMeta, null, true, new Nested(getCallback())
{
@Override
public void succeeded()