Skip to content

Commit 84c50c3

Browse files
committed
feat: Address review and Sonar findings on stop draining
1 parent ef20528 commit 84c50c3

4 files changed

Lines changed: 52 additions & 50 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -574,21 +574,23 @@ try (var server = OpenApiServer.builder()
574574
`stop(int)` and `shutdownTimeoutSeconds(int)` reject negative values with
575575
`IllegalArgumentException`.
576576

577-
Stopping closes the listening socket at once, so no new connection is accepted. Requests on
578-
keep-alive connections that are already open are served until the JDK sees no exchange in flight,
579-
which can come before the after-response runnables of the last request have finished; the JDK then
580-
closes every connection, keep-alive ones included. The wait ends as soon as `inFlightRequests()` reads `0`, or when the delay elapses; every connection
581-
is then closed, which cuts off what is still running. A request counts as in flight from the first
582-
filter until its after-response runnables (`Request.afterResponse`, hooks) have run, so `stop` waits
583-
for those too. A request the JDK rejects before it dispatches it, such as a malformed one, is not
584-
counted.
577+
Stopping closes the listening socket at once, so no new connection is accepted.
578+
Requests on keep-alive connections that are already open are still served
579+
until the JDK sees no exchange in flight.
580+
The wait ends as soon as `inFlightRequests()` reads `0`, or when the delay elapses.
581+
Every connection is then closed, which cuts off what is still running.
582+
A request counts as in flight from the first filter
583+
until its after-response runnables (`Request.afterResponse`, hooks) have run,
584+
so `stop` waits for those too.
585+
A request the JDK rejects before it dispatches it, such as a malformed one, is not counted.
585586

586587
Only the first `stop` call stops the server. A later or concurrent call waits until that stop is
587588
complete, then returns (or throws `IllegalStateException` if that stop failed); its own delay is
588589
ignored. A thread interrupted while it waits stops waiting and keeps its interrupt flag.
589590

590-
`inFlightRequests()` is the server's own count, readable at any time, for example to log it or to
591-
wait for `0` before stopping.
591+
`inFlightRequests()` is a snapshot of the server's own count, readable at any time,
592+
for example for logs and metrics.
593+
To drain before shutdown, call `stop(int)`, which waits for the count itself.
592594

593595
## Interceptors and response decorators
594596

‎src/main/java/com/retailsvc/http/OpenApiServer.java‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,10 @@ public InetAddress bindAddress() {
232232
}
233233

234234
/**
235-
* The number of requests being handled now, from the first filter to the end of the
236-
* after-response hooks. A request the JDK rejects before it dispatches it, such as a malformed
237-
* one, is not counted. Reads {@code 0} once the server is idle, so a caller can wait for that
238-
* before {@link #stop(int)}.
235+
* A snapshot of the number of requests being handled now, from the first filter to the end of the
236+
* after-response hooks, for logs and metrics. A request the JDK rejects before it dispatches it,
237+
* such as a malformed one, is not counted. To drain before shutdown, call {@link #stop(int)},
238+
* which waits for this count itself.
239239
*/
240240
public int inFlightRequests() {
241241
return counter.inFlight();
@@ -269,23 +269,23 @@ public void stop(int delaySeconds) {
269269
try {
270270
drainAndStop(delaySeconds);
271271
stopped.complete(null);
272-
} catch (RuntimeException | Error e) {
272+
} catch (RuntimeException e) {
273273
stopped.completeExceptionally(e);
274274
throw e;
275+
} finally {
276+
// An Error leaves the future open; complete it so no later call waits forever.
277+
if (!stopped.isDone()) {
278+
stopped.completeExceptionally(new IllegalStateException("The stop was aborted"));
279+
}
275280
}
276281
} else {
277282
awaitStopped();
278283
}
279284
}
280285

281286
/**
282-
* {@code HttpServer.stop(delay)} closes the listening socket at once and then waits on a latch
283-
* that, in the JDK this was written against, can stay unreleased with nothing in flight. It runs
284-
* on its own thread for the first, and {@code stop(0)} ends it: {@code ServerImpl.stop} is safe
285-
* to call again while another call waits. Each call only sets {@code terminating}, posts another
286-
* {@code StopRequested} event, closes the already closed socket and waits on the latch; its
287-
* {@code finally} counts the latch down for every waiter and clears connections and timers, which
288-
* tolerates repeats.
287+
* The first call runs {@code HttpServer.stop(delay)} on a virtual thread, which closes the
288+
* listener; {@code stop(0)} releases its latch.
289289
*/
290290
private void drainAndStop(int delaySeconds) {
291291
Thread background = ofVirtual().name("http-stop").start(() -> httpServer.stop(delaySeconds));
@@ -294,14 +294,13 @@ private void drainAndStop(int delaySeconds) {
294294
boolean interrupted = Thread.interrupted();
295295
httpServer.stop(0);
296296
try {
297-
background.join(Duration.ofSeconds(STOP_JOIN_SECONDS));
297+
if (!background.join(Duration.ofSeconds(STOP_JOIN_SECONDS))) {
298+
LOG.warn(
299+
"The JDK stop thread is still running {} s after the server was stopped",
300+
STOP_JOIN_SECONDS);
301+
}
298302
} catch (InterruptedException _) {
299-
interrupted = true;
300-
}
301-
if (background.isAlive()) {
302-
LOG.warn(
303-
"The JDK stop thread is still running {} s after the server was stopped",
304-
STOP_JOIN_SECONDS);
303+
Thread.currentThread().interrupt();
305304
}
306305
if (interrupted) {
307306
Thread.currentThread().interrupt();
@@ -314,7 +313,7 @@ private void awaitStopped() {
314313
} catch (InterruptedException _) {
315314
Thread.currentThread().interrupt();
316315
} catch (ExecutionException e) {
317-
throw new IllegalStateException("The first stop failed", e.getCause());
316+
throw new IllegalStateException("The first stop failed", e);
318317
}
319318
}
320319

‎src/main/java/com/retailsvc/http/internal/InFlightCounter.java‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,27 @@ public boolean awaitIdle(long timeout, TimeUnit unit) {
6262
return inFlight() == 0;
6363
}
6464

65-
private void enter() {
66-
lock.lock();
67-
try {
68-
inFlight++;
69-
} finally {
70-
lock.unlock();
71-
}
72-
}
65+
private final class CountingFilter extends Filter {
7366

74-
private void exit() {
75-
lock.lock();
76-
try {
77-
if (--inFlight == 0) {
78-
idle.signalAll();
67+
private void enter() {
68+
lock.lock();
69+
try {
70+
inFlight++;
71+
} finally {
72+
lock.unlock();
7973
}
80-
} finally {
81-
lock.unlock();
8274
}
83-
}
8475

85-
private final class CountingFilter extends Filter {
76+
private void exit() {
77+
lock.lock();
78+
try {
79+
if (--inFlight == 0) {
80+
idle.signalAll();
81+
}
82+
} finally {
83+
lock.unlock();
84+
}
85+
}
8686

8787
@Override
8888
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {

‎src/test/java/com/retailsvc/http/GracefulStopIT.java‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class GracefulStopIT {
5151
private static final int ROUNDS = 100;
5252
private static final long MAX_PARK_NANOS = 400_000;
5353
private static final long LONG_ENOUGH_MILLIS = 300;
54+
private static final long POLL_MILLIS = 10;
5455

5556
private final CountDownLatch release = new CountDownLatch(1);
5657
private final CountDownLatch entered = new CountDownLatch(1);
@@ -240,7 +241,7 @@ void keepsTheInterruptFlagOfAThreadInterruptedWhileStopping() throws Exception {
240241
}
241242

242243
@Test
243-
void ignoresASecondStop() {
244+
void returnsAtOnceWhenAlreadyStopped() {
244245
server = start(Map.of());
245246

246247
server.stop(0);
@@ -343,13 +344,13 @@ private static boolean isDone(CompletableFuture<?> future, long millis)
343344
}
344345

345346
/** Polls {@code condition} for up to five seconds. */
346-
private static boolean eventually(BooleanSupplier condition) throws InterruptedException {
347+
private static boolean eventually(BooleanSupplier condition) {
347348
long deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos();
348349
while (System.nanoTime() < deadline) {
349350
if (condition.getAsBoolean()) {
350351
return true;
351352
}
352-
Thread.sleep(10);
353+
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(POLL_MILLIS));
353354
}
354355
return condition.getAsBoolean();
355356
}

0 commit comments

Comments
 (0)