Skip to content

Commit dc3886a

Browse files
committed
add shutdown handler #1796
- remove shutdown handler from undertow server
1 parent da22657 commit dc3886a

File tree

5 files changed

+231
-11
lines changed

5 files changed

+231
-11
lines changed

docs/asciidoc/handlers.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ include::handlers/cors.adoc[]
88

99
include::handlers/csrf.adoc[]
1010

11+
include::handlers/graceful-shutdown.adoc[]
12+
1113
include::handlers/head.adoc[]
1214

1315
include::handlers/rate-limit.adoc[]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
=== GracefulShutdown
2+
3+
The javadoc:GracefulShutdown[] extension waits for existing requests to finish.
4+
5+
.Example
6+
[source, java, role = "primary"]
7+
----
8+
import io.jooby.Jooby;
9+
import io.jooby.GracefulShutdown;
10+
...
11+
{
12+
13+
install(new GracefulShutdown()); <1>
14+
15+
// other routes go here
16+
}
17+
----
18+
19+
.Kotlin
20+
[source, kotlin, role = "secondary"]
21+
----
22+
import io.jooby.Jooby
23+
import io.jooby.GracefulShutdown
24+
...
25+
{
26+
install(GracefulShutdown()) <1>
27+
28+
// other routes go here
29+
}
30+
----
31+
32+
<1> Install GracefulShutdown.
33+
34+
Incoming request are resolved as `Service Unavailable(503)`. Optionally you can specify a max
35+
amount of time to wait before shutdown:
36+
37+
install(new GracefulShutdown(Duration.ofMinutes(1)));
38+
39+
[INFO]
40+
====
41+
This extension must be installed at very beginning of your route pipeline.
42+
====
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import io.jooby.internal.GracefulShutdownHandler;
9+
10+
import javax.annotation.Nonnull;
11+
import java.time.Duration;
12+
13+
/**
14+
* Install a handler that at application shutdown time:
15+
*
16+
* - Waits for existing requests to finished with an optional timeout
17+
* - Incoming requests are resolved as Service Unavailable(503)
18+
*
19+
* NOTE: This extension must be installed at very beginning of your route pipeline.
20+
*
21+
* @author edgar
22+
*/
23+
public class GracefulShutdown implements Extension {
24+
private Duration await;
25+
26+
/**
27+
* Creates a new shutdown handler and waits for existing requests to finish or for specified
28+
* amount of time.
29+
*
30+
* @param await Max time to wait for handlers to complete.
31+
*/
32+
public GracefulShutdown(@Nonnull Duration await) {
33+
this.await = await;
34+
}
35+
36+
/**
37+
* Creates a new shutdown handler and waits for existing request to finish.
38+
*/
39+
public GracefulShutdown() {
40+
}
41+
42+
@Override public void install(@Nonnull Jooby application) throws Exception {
43+
GracefulShutdownHandler handler = new GracefulShutdownHandler(await);
44+
application.decorator(handler);
45+
application.onStop(handler::shutdown);
46+
}
47+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal;
7+
8+
import io.jooby.Route;
9+
import io.jooby.StatusCode;
10+
11+
import javax.annotation.Nonnull;
12+
import java.time.Duration;
13+
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
14+
import java.util.function.LongUnaryOperator;
15+
16+
public class GracefulShutdownHandler implements Route.Decorator {
17+
private static final long SHUTDOWN_MASK = 1L << 63;
18+
private static final long ACTIVE_COUNT_MASK = (1L << 63) - 1;
19+
20+
private static final LongUnaryOperator incrementActive = current -> {
21+
long incrementedActiveCount = activeCount(current) + 1;
22+
return incrementedActiveCount | (current & ~ACTIVE_COUNT_MASK);
23+
};
24+
25+
private static final LongUnaryOperator incrementActiveAndShutdown =
26+
incrementActive.andThen(current -> current | SHUTDOWN_MASK);
27+
28+
private static final LongUnaryOperator decrementActive = current -> {
29+
long decrementedActiveCount = activeCount(current) - 1;
30+
return decrementedActiveCount | (current & ~ACTIVE_COUNT_MASK);
31+
};
32+
33+
private final Object lock = new Object();
34+
35+
private final Duration await;
36+
37+
private volatile long state = 0;
38+
private static final AtomicLongFieldUpdater<GracefulShutdownHandler> stateUpdater =
39+
AtomicLongFieldUpdater.newUpdater(GracefulShutdownHandler.class, "state");
40+
41+
public GracefulShutdownHandler(Duration await) {
42+
this.await = await;
43+
}
44+
45+
@Nonnull @Override public Route.Handler apply(@Nonnull Route.Handler next) {
46+
return ctx -> {
47+
long snapshot = stateUpdater.updateAndGet(this, incrementActive);
48+
if (isShutdown(snapshot)) {
49+
decrementRequests();
50+
return ctx.setResponseCode(StatusCode.SERVICE_UNAVAILABLE)
51+
.send(StatusCode.SERVICE_UNAVAILABLE);
52+
} else {
53+
ctx.onComplete(context -> {
54+
decrementRequests();
55+
});
56+
return next.apply(ctx);
57+
}
58+
};
59+
}
60+
61+
private static boolean isShutdown(long state) {
62+
return (state & SHUTDOWN_MASK) != 0;
63+
}
64+
65+
private static long activeCount(long state) {
66+
return state & ACTIVE_COUNT_MASK;
67+
}
68+
69+
public void shutdown() throws InterruptedException {
70+
//the request count is never zero when shutdown is set to true
71+
stateUpdater.updateAndGet(this, incrementActiveAndShutdown);
72+
decrementRequests();
73+
74+
if (await == null) {
75+
awaitShutdown();
76+
} else {
77+
awaitShutdown(await.toMillis());
78+
}
79+
}
80+
81+
public void start() {
82+
synchronized (lock) {
83+
stateUpdater.updateAndGet(this, current -> current & ACTIVE_COUNT_MASK);
84+
}
85+
}
86+
87+
private void shutdownComplete() {
88+
synchronized (lock) {
89+
lock.notifyAll();
90+
}
91+
}
92+
93+
/**
94+
* Waits for the handler to shutdown.
95+
*/
96+
private void awaitShutdown() throws InterruptedException {
97+
synchronized (lock) {
98+
if (!isShutdown(stateUpdater.get(this))) {
99+
return;
100+
}
101+
while (activeCount(stateUpdater.get(this)) > 0) {
102+
lock.wait();
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Waits a set length of time for the handler to shut down
109+
*
110+
* @param millis The length of time
111+
* @return <code>true</code> If the handler successfully shut down
112+
*/
113+
private boolean awaitShutdown(long millis) throws InterruptedException {
114+
synchronized (lock) {
115+
if (!isShutdown(stateUpdater.get(this))) {
116+
return false;
117+
}
118+
long end = System.currentTimeMillis() + millis;
119+
while (activeCount(stateUpdater.get(this)) != 0) {
120+
long left = end - System.currentTimeMillis();
121+
if (left <= 0) {
122+
return false;
123+
}
124+
lock.wait(left);
125+
}
126+
return true;
127+
}
128+
}
129+
130+
private void decrementRequests() {
131+
long snapshot = stateUpdater.updateAndGet(this, decrementActive);
132+
// Shutdown has completed when the activeCount portion is zero, and shutdown is set.
133+
if (snapshot == SHUTDOWN_MASK) {
134+
shutdownComplete();
135+
}
136+
}
137+
}

modules/jooby-utow/src/main/java/io/jooby/utow/Utow.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import io.undertow.Undertow;
1414
import io.undertow.UndertowOptions;
1515
import io.undertow.server.HttpHandler;
16-
import io.undertow.server.handlers.GracefulShutdownHandler;
1716
import io.undertow.server.handlers.encoding.ContentEncodingRepository;
1817
import io.undertow.server.handlers.encoding.DeflateEncodingProvider;
1918
import io.undertow.server.handlers.encoding.EncodingHandler;
@@ -27,7 +26,6 @@
2726
import java.util.Collections;
2827
import java.util.List;
2928
import java.util.Optional;
30-
import java.util.concurrent.TimeUnit;
3129

3230
/**
3331
* Web server implementation using <a href="http://undertow.io/">Undertow</a>.
@@ -45,8 +43,6 @@ public class Utow extends Server.Base {
4543

4644
private Undertow server;
4745

48-
private GracefulShutdownHandler shutdown;
49-
5046
private List<Jooby> applications = new ArrayList<>();
5147

5248
private ServerOptions options = new ServerOptions()
@@ -80,8 +76,6 @@ public class Utow extends Server.Base {
8076
.addEncodingHandler("deflate", new DeflateEncodingProvider(compressionLevel), _10));
8177
}
8278

83-
shutdown = new GracefulShutdownHandler(handler);
84-
8579
Undertow.Builder builder = Undertow.builder()
8680
.addHttpListener(options.getPort(), options.getHost())
8781
.setBufferSize(options.getBufferSize())
@@ -98,7 +92,7 @@ public class Utow extends Server.Base {
9892
.setIoThreads(options.getIoThreads())
9993
.setWorkerOption(Options.WORKER_NAME, "worker")
10094
.setWorkerThreads(options.getWorkerThreads())
101-
.setHandler(shutdown);
95+
.setHandler(handler);
10296

10397
SSLContext sslContext = options.getSSLContext(application.getEnvironment().getClassLoader());
10498
if (sslContext != null) {
@@ -123,11 +117,9 @@ public class Utow extends Server.Base {
123117
}
124118

125119
@Nonnull @Override public synchronized Server stop() {
126-
fireStop(applications);
127-
applications = null;
128120
try {
129-
shutdown.shutdown();
130-
shutdown.awaitShutdown(TimeUnit.SECONDS.toMillis(1));
121+
fireStop(applications);
122+
applications = null;
131123
} catch (Exception x) {
132124
throw SneakyThrows.propagate(x);
133125
} finally {

0 commit comments

Comments
 (0)