Skip to content

Commit c1fadb2

Browse files
committed
[bugfix] Fix Jetty shutdown hang with setStopTimeout and direct shutdown
- Add Server instance field to JettyStart for direct lifecycle access - Set stop timeout (30s) on Server so server.stop() never hangs - Shutdown Jetty directly in shutdown() instead of via ShutdownListenerImpl timer chain (BrokerPool.stopAll → listener → 1s Timer → server.stop) - Keeps deadline-based wait(remaining) as defense-in-depth - Removes unused Timer/TimerTask imports Closes the test suite hang where Object.wait() in JettyStart.shutdown() waited indefinitely for lifeCycleStopped() that never fired.
1 parent db4a1bb commit c1fadb2

1 file changed

Lines changed: 44 additions & 36 deletions

File tree

exist-core/src/main/java/org/exist/jetty/JettyStart.java

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,16 @@
5656
import java.net.*;
5757
import java.nio.file.Files;
5858
import java.nio.file.Path;
59-
import java.util.*;
59+
import java.util.ArrayList;
60+
import java.util.Arrays;
61+
import java.util.HashMap;
62+
import java.util.List;
63+
import java.util.Map;
64+
import java.util.Objects;
65+
import java.util.Observer;
66+
import java.util.Optional;
67+
import java.util.Properties;
68+
import java.util.ServiceLoader;
6069
import java.util.concurrent.CopyOnWriteArrayList;
6170
import java.util.concurrent.CountDownLatch;
6271
import java.util.concurrent.TimeUnit;
@@ -105,6 +114,7 @@ public class JettyStart implements LifeCycle.Listener {
105114

106115
@GuardedBy("this") private int status = STATUS_STOPPED;
107116
@GuardedBy("this") private Optional<Thread> shutdownHookThread = Optional.empty();
117+
@GuardedBy("this") private Optional<Server> server = Optional.empty();
108118
@GuardedBy("this") private int primaryPort = 8080;
109119
@GuardedBy("this") private boolean webAppStartedSuccessfully = false;
110120
@GuardedBy("this") private String webAppStartupFailureDetail = null;
@@ -518,18 +528,18 @@ private URI getURI(final NetworkConnector networkConnector, final ContextHandler
518528

519529
private Optional<Server> startJetty(final List<Object> configuredObjects) throws Exception {
520530
// For all objects created by XmlConfigurations, start them if they are lifecycles.
521-
Optional<Server> server = Optional.empty();
531+
Optional<Server> serverFound = Optional.empty();
522532
for (final Object configuredObject : configuredObjects) {
523533
if(configuredObject instanceof Server _server) {
524534

525535
//skip this server if we have already started it
526-
if(server.map(configuredServer -> configuredServer == _server).orElse(false)) {
536+
if(serverFound.map(configuredServer -> configuredServer == _server).orElse(false)) {
527537
continue;
528538
}
529539

530540
//setup server shutdown
531541
_server.addEventListener(this);
532-
BrokerPool.getInstance().registerShutdownListener(new ShutdownListenerImpl(_server));
542+
BrokerPool.getInstance().registerShutdownListener(new ShutdownListenerImpl());
533543

534544
// register a shutdown hook for the server
535545
final BrokerPoolAndJettyShutdownHook brokerPoolAndJettyShutdownHook =
@@ -550,7 +560,9 @@ private Optional<Server> startJetty(final List<Object> configuredObjects) throws
550560
throw e;
551561
}
552562

553-
server = Optional.of(_server);
563+
_server.setStopTimeout(TimeUnit.SECONDS.toMillis(30));
564+
serverFound = Optional.of(_server);
565+
this.server = serverFound;
554566
}
555567

556568
if (configuredObject instanceof LifeCycle lc && !lc.isRunning()) {
@@ -559,7 +571,7 @@ private Optional<Server> startJetty(final List<Object> configuredObjects) throws
559571
}
560572
}
561573

562-
return server;
574+
return serverFound;
563575
}
564576

565577
/**
@@ -854,9 +866,25 @@ public synchronized void shutdown() {
854866

855867
BrokerPool.stopAll(false);
856868

869+
// stop the Jetty server directly (no longer relies on ShutdownListenerImpl timer)
870+
server.ifPresent(srv -> {
871+
try {
872+
srv.stop();
873+
srv.join();
874+
} catch (final Exception e) {
875+
logger.warn("Error stopping Jetty server: {}", e.getMessage(), e);
876+
}
877+
});
878+
879+
final long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30);
857880
while (status != STATUS_STOPPED) {
881+
final long remaining = deadline - System.currentTimeMillis();
882+
if (remaining <= 0) {
883+
logger.warn("Timed out waiting for Jetty to reach STATUS_STOPPED (current status={}); forcing shutdown", status);
884+
break;
885+
}
858886
try {
859-
wait();
887+
wait(remaining);
860888
} catch (final InterruptedException e) {
861889
Thread.currentThread().interrupt();
862890
}
@@ -865,38 +893,12 @@ public synchronized void shutdown() {
865893

866894
/**
867895
* This class gets called after the database received a shutdown request.
868-
*
869-
* @author wolf
896+
* Server stop is handled directly in {@link #shutdown()}.
870897
*/
871898
private static class ShutdownListenerImpl implements ShutdownListener {
872-
private final Server server;
873-
874-
ShutdownListenerImpl(final Server server) {
875-
this.server = server;
876-
}
877-
878899
@Override
879900
public void shutdown(final String dbname, final int remainingInstances) {
880-
logger.info("Database shutdown: stopping server in 1sec ...");
881-
if (remainingInstances == 0) {
882-
// give the webserver a 1s chance to complete open requests
883-
final Timer timer = new Timer("jetty shutdown schedule", true);
884-
timer.schedule(new TimerTask() {
885-
@Override
886-
public void run() {
887-
try {
888-
// stop the server
889-
server.stop();
890-
server.join();
891-
892-
// make sure to stop the timer thread!
893-
timer.cancel();
894-
} catch (final Exception e) {
895-
logger.error("An error occurred in the shutdown scheduler: {}", e.getMessage(), e);
896-
}
897-
}
898-
}, 1000); // timer.schedule
899-
}
901+
logger.info("Database shutdown: stopping server ...");
900902
}
901903
}
902904

@@ -929,9 +931,15 @@ public synchronized boolean isStarted() {
929931
if (status == STATUS_STOPPED) {
930932
return false;
931933
}
934+
final long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30);
932935
while (status != STATUS_STOPPED) {
936+
final long remaining = deadline - System.currentTimeMillis();
937+
if (remaining <= 0) {
938+
logger.warn("Timed out waiting for Jetty to reach STATUS_STOPPED (current status={})", status);
939+
return false;
940+
}
933941
try {
934-
wait();
942+
wait(remaining);
935943
} catch (final InterruptedException e) {
936944
Thread.currentThread().interrupt();
937945
}

0 commit comments

Comments
 (0)