Skip to content

Commit d2c4cc8

Browse files
committed
[refactor] index worker priorities, Jetty listener API, test server builder
- IndexWorker.getChainPriority() replaces IndexController substring ranking - JettyStartListener replaces deprecated Observable for Jetty lifecycle signals - Event-driven webapp readiness wait with CountDownLatch - org.exist.jetty.startup.timeout.ms override; EXIST/PORTAL context constants - ExistWebServer.builder() for readable test configuration
1 parent 0dd2368 commit d2c4cc8

13 files changed

Lines changed: 365 additions & 115 deletions

File tree

exist-core/src/main/java/org/exist/indexing/IndexController.java

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
import java.util.ArrayList;
4343
import java.util.Comparator;
4444
import java.util.HashMap;
45+
import java.util.LinkedHashMap;
4546
import java.util.List;
4647
import java.util.Map;
47-
import java.util.TreeMap;
4848
import org.exist.security.PermissionDeniedException;
4949

5050
/**
@@ -60,14 +60,12 @@ public enum CollectionIndexRemovalMode {
6060

6161
/**
6262
* Stable iteration order for listener chains and {@link #flush()}.
63-
* Alphabetical {@link TreeMap} put Lucene first and broke store-time indexing
64-
* (see {@code LuceneTests} facet/field-type tests). Core indexes run before Lucene.
6563
*/
66-
private static final Comparator<String> INDEX_WORKER_ORDER = Comparator
67-
.comparingInt(IndexController::indexWorkerChainRank)
68-
.thenComparing(Comparator.naturalOrder());
64+
private static final Comparator<IndexWorker> INDEX_WORKER_ORDER = Comparator
65+
.comparingInt(IndexWorker::getChainPriority)
66+
.thenComparing(IndexWorker::getIndexId);
6967

70-
private final Map<String, IndexWorker> indexWorkers = new TreeMap<>(INDEX_WORKER_ORDER);
68+
private final Map<String, IndexWorker> indexWorkers = new LinkedHashMap<>();
7169

7270
private final DBBroker broker;
7371
private StreamListener listener = null;
@@ -79,26 +77,9 @@ public enum CollectionIndexRemovalMode {
7977
public IndexController(final DBBroker broker) {
8078
this.broker = broker;
8179
final List<IndexWorker> workers = broker.getBrokerPool().getIndexManager().getWorkers(broker);
82-
for (final IndexWorker worker : workers) {
83-
indexWorkers.put(worker.getIndexId(), worker);
84-
}
85-
}
86-
87-
/**
88-
* Preferred {@link StreamListener} chain order: structural → statistics → Lucene → others.
89-
* Uses index-id substrings so exist-core does not depend on extension modules.
90-
*/
91-
private static int indexWorkerChainRank(final String indexId) {
92-
if (indexId.contains("NativeStructuralIndex")) {
93-
return 0;
94-
}
95-
if (indexId.contains("IndexStatistics")) {
96-
return 1;
97-
}
98-
if (indexId.contains("lucene.LuceneIndex")) {
99-
return 2;
100-
}
101-
return 3;
80+
workers.stream()
81+
.sorted(INDEX_WORKER_ORDER)
82+
.forEach(worker -> indexWorkers.put(worker.getIndexId(), worker));
10283
}
10384

10485
/**

exist-core/src/main/java/org/exist/indexing/IndexWorker.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ public interface IndexWorker {
5353
*/
5454
public static final String VALUE_COUNT = "value_count";
5555

56+
/**
57+
* Lower values run earlier in {@link IndexController} listener chains and {@link #flush()}.
58+
*/
59+
int CHAIN_PRIORITY_STRUCTURAL = 0;
60+
int CHAIN_PRIORITY_STATISTICS = 100;
61+
int CHAIN_PRIORITY_LUCENE = 1000;
62+
63+
/**
64+
* Chain-order priority for {@link IndexController}. Default {@link Integer#MAX_VALUE} preserves
65+
* legacy ordering among workers that do not override.
66+
*/
67+
default int getChainPriority() {
68+
return Integer.MAX_VALUE;
69+
}
70+
5671
/**
5772
* Returns an ID which uniquely identifies this worker's index.
5873
* @return a unique name identifying this worker's index.

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

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757
import java.nio.file.Files;
5858
import java.nio.file.Path;
5959
import java.util.*;
60+
import java.util.concurrent.CopyOnWriteArrayList;
61+
import java.util.concurrent.CountDownLatch;
62+
import java.util.concurrent.TimeUnit;
63+
import java.util.concurrent.atomic.AtomicReference;
6064
import java.util.stream.Collectors;
6165

6266
import static org.exist.util.ThreadUtils.newGlobalThread;
@@ -69,10 +73,14 @@
6973
*
7074
* @author wolf
7175
*/
72-
public class JettyStart extends Observable implements LifeCycle.Listener {
76+
public class JettyStart implements LifeCycle.Listener {
7377

7478
public static final String JETTY_HOME_PROP = "jetty.home";
7579
public static final String JETTY_BASE_PROP = "jetty.base";
80+
public static final String STARTUP_TIMEOUT_MS_PROPERTY = "org.exist.jetty.startup.timeout.ms";
81+
82+
private static final String EXIST_CONTEXT_PATH = "/exist";
83+
private static final String PORTAL_CONTEXT_PATH = "/";
7684

7785
private static final String JETTY_PROPETIES_FILENAME = "jetty.properties";
7886
private static final Logger logger = LogManager.getLogger(JettyStart.class);
@@ -101,6 +109,8 @@ public class JettyStart extends Observable implements LifeCycle.Listener {
101109
@GuardedBy("this") private boolean webAppStartedSuccessfully = false;
102110
@GuardedBy("this") private String webAppStartupFailureDetail = null;
103111

112+
private final CopyOnWriteArrayList<JettyStartListener> jettyStartListeners = new CopyOnWriteArrayList<>();
113+
104114

105115
public static void main(final String[] args) {
106116
try {
@@ -168,7 +178,25 @@ public synchronized void run(final boolean standalone) {
168178
run(new String[] { jettyConfig.toAbsolutePath().toString() }, null);
169179
}
170180

171-
public synchronized void run(final String[] args, final Observer observer) {
181+
public void addJettyStartListener(final JettyStartListener listener) {
182+
if (listener != null) {
183+
jettyStartListeners.addIfAbsent(listener);
184+
}
185+
}
186+
187+
public void removeJettyStartListener(final JettyStartListener listener) {
188+
if (listener != null) {
189+
jettyStartListeners.remove(listener);
190+
}
191+
}
192+
193+
private void notifyJettyStartListeners(final String signal) {
194+
for (final JettyStartListener listener : jettyStartListeners) {
195+
listener.onJettyStartEvent(signal);
196+
}
197+
}
198+
199+
public synchronized void run(final String[] args, final JettyStartListener listener) {
172200
if (args.length == 0) {
173201
logger.error("No configuration file specified!");
174202
return;
@@ -209,8 +237,8 @@ public synchronized void run(final String[] args, final Observer observer) {
209237
configProperties.put(JETTY_BASE_PROP, jettyClasspathHome);
210238
}
211239

212-
if (observer != null) {
213-
addObserver(observer);
240+
if (listener != null) {
241+
addJettyStartListener(listener);
214242
}
215243

216244
logger.info("Running with Java {} [{} ({}) in {}]",
@@ -249,7 +277,10 @@ public synchronized void run(final String[] args, final Observer observer) {
249277
.map(Path::normalize).map(Path::toAbsolutePath).map(Path::toString)
250278
.orElse("<UNKNOWN>"));
251279

252-
BrokerPool.configure(1, 5, config, Optional.ofNullable(observer));
280+
final Optional<Observer> brokerPoolObserver = listener instanceof Observer observer
281+
? Optional.of(observer)
282+
: Optional.empty();
283+
BrokerPool.configure(1, 5, config, brokerPoolObserver);
253284

254285
// register the XMLDB driver
255286
final Database xmldb = new DatabaseImpl();
@@ -362,13 +393,11 @@ public synchronized void run(final String[] args, final Observer observer) {
362393
webAppStartedSuccessfully = true;
363394
webAppStartupFailureDetail = null;
364395

365-
setChanged();
366-
notifyObservers(SIGNAL_STARTED);
396+
notifyJettyStartListeners(SIGNAL_STARTED);
367397

368398
} catch (final SocketException e) {
369399
recordStartupFailure("Could not bind to port: " + e.getMessage(), e);
370-
setChanged();
371-
notifyObservers(SIGNAL_ERROR);
400+
notifyJettyStartListeners(SIGNAL_ERROR);
372401

373402
} catch (final Exception e) {
374403
if (webAppStartupFailureDetail == null) {
@@ -377,8 +406,7 @@ public synchronized void run(final String[] args, final Observer observer) {
377406
} else {
378407
recordStartupFailure(webAppStartupFailureDetail, e);
379408
}
380-
setChanged();
381-
notifyObservers(SIGNAL_ERROR);
409+
notifyJettyStartListeners(SIGNAL_ERROR);
382410
}
383411
}
384412

@@ -529,9 +557,9 @@ private Optional<Server> startJetty(final List<Object> configuredObjects) throws
529557
/**
530558
* Block until deployed webapps reach the readiness level required for tests.
531559
* <p>
532-
* Every context except the distribution portal at {@code /} must be
560+
* Every context except the distribution portal at {@link #PORTAL_CONTEXT_PATH} must be
533561
* {@link org.eclipse.jetty.server.handler.ContextHandler#isAvailable()} — Jetty returns
534-
* {@code 503} on all paths while unavailable. The portal coexists with {@code /exist} and is
562+
* {@code 503} on all paths while unavailable. The portal coexists with {@link #EXIST_CONTEXT_PATH} and is
535563
* non-gating.
536564
*/
537565
private void awaitWebAppContextsStarted(final List<Handler> handlers) throws InterruptedException {
@@ -547,29 +575,87 @@ private void awaitWebAppContextsStarted(final List<Handler> handlers) throws Int
547575

548576
final boolean distributionLayout = isDistributionLayout(webApps);
549577
final long timeoutMs = slowEnvironmentStartupDeadlineMs();
550-
final long deadline = System.currentTimeMillis() + timeoutMs;
551-
while (System.currentTimeMillis() < deadline) {
552-
boolean allReady = true;
578+
final CountDownLatch readyLatch = new CountDownLatch(1);
579+
final AtomicReference<IllegalStateException> failure = new AtomicReference<>();
580+
581+
final LifeCycle.Listener readinessListener = new LifeCycle.Listener() {
582+
@Override
583+
public void lifeCycleStarted(final LifeCycle event) {
584+
evaluateReadiness();
585+
}
586+
587+
@Override
588+
public void lifeCycleFailure(final LifeCycle event, final Throwable cause) {
589+
if (event instanceof WebAppContext webApp) {
590+
failure.compareAndSet(null, new IllegalStateException(
591+
"Web application failed to start: " + webApp.getContextPath(), cause));
592+
} else {
593+
failure.compareAndSet(null, new IllegalStateException("Web application failed to start", cause));
594+
}
595+
readyLatch.countDown();
596+
}
597+
598+
private void evaluateReadiness() {
599+
for (final WebAppContext webApp : webApps) {
600+
if (webApp.isFailed()) {
601+
failure.compareAndSet(null, new IllegalStateException(
602+
"Web application failed to start: " + webApp.getContextPath()));
603+
readyLatch.countDown();
604+
return;
605+
}
606+
}
607+
if (allWebAppsReady(webApps, distributionLayout)) {
608+
readyLatch.countDown();
609+
}
610+
}
611+
};
612+
613+
for (final WebAppContext webApp : webApps) {
614+
webApp.addEventListener(readinessListener);
615+
}
616+
617+
try {
618+
if (allWebAppsReady(webApps, distributionLayout)) {
619+
logger.info("All required web application contexts are ready.");
620+
return;
621+
}
553622
for (final WebAppContext webApp : webApps) {
554623
if (webApp.isFailed()) {
555624
throw new IllegalStateException(
556625
"Web application failed to start: " + webApp.getContextPath());
557626
}
558-
if (!isWebAppContextReady(webApp, distributionLayout)) {
559-
allReady = false;
560-
break;
561-
}
562627
}
563-
if (allReady) {
564-
logger.info("All required web application contexts are ready.");
565-
return;
628+
if (!readyLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
629+
throw new IllegalStateException(
630+
"Web application context did not become ready within " + timeoutMs + "ms: "
631+
+ describePendingWebApps(webApps, distributionLayout),
632+
firstUnavailableCause(webApps));
633+
}
634+
final IllegalStateException startupFailure = failure.get();
635+
if (startupFailure != null) {
636+
throw startupFailure;
637+
}
638+
if (!allWebAppsReady(webApps, distributionLayout)) {
639+
throw new IllegalStateException(
640+
"Web application context did not become ready: "
641+
+ describePendingWebApps(webApps, distributionLayout),
642+
firstUnavailableCause(webApps));
643+
}
644+
logger.info("All required web application contexts are ready.");
645+
} finally {
646+
for (final WebAppContext webApp : webApps) {
647+
webApp.removeEventListener(readinessListener);
648+
}
649+
}
650+
}
651+
652+
private static boolean allWebAppsReady(final List<WebAppContext> webApps, final boolean distributionLayout) {
653+
for (final WebAppContext webApp : webApps) {
654+
if (!isWebAppContextReady(webApp, distributionLayout)) {
655+
return false;
566656
}
567-
Thread.sleep(200);
568657
}
569-
throw new IllegalStateException(
570-
"Web application context did not become ready within " + timeoutMs + "ms: "
571-
+ describePendingWebApps(webApps, distributionLayout),
572-
firstUnavailableCause(webApps));
658+
return true;
573659
}
574660

575661
private static Throwable firstUnavailableCause(final List<WebAppContext> webApps) {
@@ -583,18 +669,18 @@ private static Throwable firstUnavailableCause(final List<WebAppContext> webApps
583669
}
584670

585671
private static boolean isDistributionLayout(final List<WebAppContext> webApps) {
586-
return webApps.stream().anyMatch(webApp -> "/exist".equals(webApp.getContextPath()));
672+
return webApps.stream().anyMatch(webApp -> EXIST_CONTEXT_PATH.equals(webApp.getContextPath()));
587673
}
588674

589675
/**
590-
* Distribution portal {@code /} only needs {@code isStarted()}. Standalone {@code /} and
591-
* {@code /exist} must be {@code isAvailable()} or HTTP clients see {@code 503}.
676+
* Distribution portal {@link #PORTAL_CONTEXT_PATH} only needs {@code isStarted()}. Standalone
677+
* {@link #PORTAL_CONTEXT_PATH} and {@link #EXIST_CONTEXT_PATH} must be {@code isAvailable()} or HTTP clients see {@code 503}.
592678
*/
593679
private static boolean isWebAppContextReady(final WebAppContext webApp, final boolean distributionLayout) {
594680
if (!webApp.isStarted()) {
595681
return false;
596682
}
597-
if (distributionLayout && "/".equals(webApp.getContextPath())) {
683+
if (distributionLayout && PORTAL_CONTEXT_PATH.equals(webApp.getContextPath())) {
598684
return true;
599685
}
600686
return webApp.isAvailable();
@@ -639,10 +725,14 @@ private static String describeWebAppWar(final WebAppContext webApp) {
639725
}
640726

641727
private static boolean requiresAvailability(final WebAppContext webApp, final boolean distributionLayout) {
642-
return !(distributionLayout && "/".equals(webApp.getContextPath()));
728+
return !(distributionLayout && PORTAL_CONTEXT_PATH.equals(webApp.getContextPath()));
643729
}
644730

645731
private static long slowEnvironmentStartupDeadlineMs() {
732+
final String override = System.getProperty(STARTUP_TIMEOUT_MS_PROPERTY);
733+
if (override != null && !override.isBlank()) {
734+
return Long.parseLong(override);
735+
}
646736
if (System.getenv("CI") != null) {
647737
return 180_000L;
648738
}
@@ -799,17 +889,15 @@ public synchronized boolean isStarted() {
799889
@Override
800890
public synchronized void lifeCycleStarting(final LifeCycle lifeCycle) {
801891
logger.info("Jetty server starting...");
802-
setChanged();
803-
notifyObservers(SIGNAL_STARTING);
892+
notifyJettyStartListeners(SIGNAL_STARTING);
804893
status = STATUS_STARTING;
805894
notifyAll();
806895
}
807896

808897
@Override
809898
public synchronized void lifeCycleStarted(final LifeCycle lifeCycle) {
810899
logger.info("Jetty server started.");
811-
setChanged();
812-
notifyObservers(SIGNAL_STARTED);
900+
notifyJettyStartListeners(SIGNAL_STARTED);
813901
status = STATUS_STARTED;
814902
notifyAll();
815903
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
* info@exist-db.org
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
package org.exist.jetty;
23+
24+
/**
25+
* Receives lifecycle signals from {@link JettyStart} (replacing deprecated {@code java.util.Observer}).
26+
*/
27+
@FunctionalInterface
28+
public interface JettyStartListener {
29+
30+
void onJettyStartEvent(String signal);
31+
}

0 commit comments

Comments
 (0)