From 375b5be8fb3c2e796f79826d89985fda3614585a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 5 Aug 2026 01:16:52 +0200 Subject: [PATCH 1/5] Fix bin/solr.cmd start waiting for all running Solr instances On Windows, 'solr.cmd start' invoked 'status --max-wait-secs' without identifying the instance just started, causing the status tool to scan the PID directory and wait for every running Solr instance. Pass the port so only the newly started instance is waited for, matching the behavior of bin/solr on Linux. The status tool resolves the URL scheme (http/https) from the running process itself. --- solr/bin/solr.cmd | 2 +- solr/core/src/java/org/apache/solr/cli/StatusTool.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index 743d8fb7cdc3..778509f7e12d 100755 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -1178,7 +1178,7 @@ IF "%FG%"=="1" ( "%JAVA%" %SOLR_SSL_OPTS% %AUTHC_OPTS% %SOLR_ZK_CREDS_AND_ACLS% %SOLR_TOOL_OPTS% -Dsolr.install.dir="%SOLR_TIP%" ^ -Dlog4j.configurationFile="file:///%DEFAULT_SERVER_DIR%\resources\log4j2-console.xml" ^ -classpath "%SOLR_TIP%\lib\*;%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^ - org.apache.solr.cli.SolrCLI status --max-wait-secs !SOLR_START_WAIT! + org.apache.solr.cli.SolrCLI status -p %SOLR_PORT_LISTEN% --max-wait-secs !SOLR_START_WAIT! IF NOT "!ERRORLEVEL!"=="0" ( set "SCRIPT_ERROR=Solr did not start or was not reachable. Check the logs for errors." goto err diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 27a4840a3a4e..228908d8fb30 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -113,7 +113,6 @@ public void runImpl(CommandLine cli) throws Exception { // URL provided, do not consult local processes, as the URL may be remote if (maxWaitSecs > 0) { - // Used by Windows start script when starting Solr try { waitForSolrUpAndPrintStatus(solrUrl, cli, maxWaitSecs); runtime.exit(0); @@ -168,6 +167,7 @@ private void printProcessStatus(SolrProcess process, CommandLine cli) throws Exc CLIO.out(pidUrl); } else { if (maxWaitSecs > 0) { + // Used by Windows start script, which passes the port of the newly started instance waitForSolrUpAndPrintStatus(pidUrl, cli, maxWaitSecs); } else { CLIO.out( From 40184885b2b49916d6efd2bff4fc481bd378b1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 5 Aug 2026 10:20:49 +0200 Subject: [PATCH 2/5] Use bind host instead of localhost when connecting to local Solr process With SOLR_HOST_BIND set to a non-loopback address, Jetty does not listen on localhost, so the status tool could not reach the instance when waiting for startup. Parse the bind host from the process command line (like port and SSL already are) and use it in the local URL. Wildcard and loopback bind addresses still map to localhost, preserving existing URLs for default installs and keeping SSL hostname verification working with certificates issued for localhost. --- .../apache/solr/cli/SolrProcessManager.java | 37 ++++++++++-- .../solr/cli/SolrProcessManagerTest.java | 59 ++++++++++++------- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java index 3001fd2afd53..8052cbf07ea9 100644 --- a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java @@ -73,7 +73,10 @@ public SolrProcessManager() { ProcessHandle::pid, ph -> new SolrProcess( - ph.pid(), parsePortFromProcess(ph).orElseThrow(), isProcessSsl(ph)))); + ph.pid(), + parsePortFromProcess(ph).orElseThrow(), + isProcessSsl(ph), + localConnectHost(parseBindHostFromProcess(ph))))); portProcessMap = pidProcessMap.values().stream().collect(Collectors.toUnmodifiableMap(p -> p.port, p -> p)); String solrInstallDir = EnvUtils.getProperty(SOLR_INSTALL_DIR); @@ -152,6 +155,28 @@ private Optional parsePortFromProcess(ProcessHandle ph) { return portStr.isPresent() ? portStr.map(Integer::parseInt) : Optional.empty(); } + private Optional parseBindHostFromProcess(ProcessHandle ph) { + return arguments(ph).stream() + .filter(a -> a.contains("-Dsolr.host.bind=") || a.contains("-Dsolr.jetty.host=")) + .map(s -> s.split("=", 2)[1]) + .findFirst(); + } + + /** + * Resolves the host to use when connecting locally to a Solr process: the host the process is + * bound to if it is a specific non-loopback address, otherwise {@code localhost}. Wildcard and + * loopback binds are reachable as {@code localhost}, which also keeps SSL hostname verification + * working with certificates issued for {@code localhost}. IPv6 literals are bracketed for use in + * URLs. + */ + static String localConnectHost(Optional bindHost) { + String host = bindHost.map(String::trim).orElse(""); + return switch (host) { + case "", "0.0.0.0", "::", "[::]", "127.0.0.1", "::1", "[::1]", "localhost" -> "localhost"; + default -> host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; + }; + } + private boolean isProcessSsl(ProcessHandle ph) { return arguments(ph).stream() .anyMatch( @@ -237,11 +262,15 @@ private static List arguments(ProcessHandle ph) { } } - /** Represents a running Solr process */ - public record SolrProcess(long pid, int port, boolean isHttps) { + /** + * Represents a running Solr process. The {@code host} is the host to use when connecting to the + * process from the local machine, i.e. the bind host if bound to a specific address, else {@code + * localhost}. + */ + public record SolrProcess(long pid, int port, boolean isHttps, String host) { public String getLocalUrl() { - return String.format(Locale.ROOT, "%s://localhost:%s/solr", isHttps ? "https" : "http", port); + return String.format(Locale.ROOT, "%s://%s:%s/solr", isHttps ? "https" : "http", host, port); } } } diff --git a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java index 2102e1e3d75d..b95b57d9a02b 100644 --- a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java +++ b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java @@ -26,9 +26,12 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.math3.util.Pair; import org.apache.solr.SolrTestCase; @@ -50,8 +53,8 @@ public static void beforeClass() throws Exception { boolean isWindows = random().nextBoolean(); String PID_SUFFIX = isWindows ? ".port" : ".pid"; log.info("Simulating pid file on {}", isWindows ? "Windows" : "Linux"); - processHttp = createProcess(findAvailablePort(), false); - processHttps = createProcess(findAvailablePort(), true); + processHttp = createProcess(findAvailablePort(), false, null); + processHttps = createProcess(findAvailablePort(), true, "127.0.0.1"); long processHttpValue = isWindows ? processHttp.getKey() : processHttp.getValue().pid(); long processHttpsValue = isWindows ? processHttps.getKey() : processHttps.getValue().pid(); SolrProcessManager.enableTestingMode = true; @@ -80,7 +83,8 @@ private static int findAvailablePort() throws IOException { } @SuppressWarnings("SystemGetProperty") - private static Pair createProcess(int port, boolean https) throws IOException { + private static Pair createProcess(int port, boolean https, String bindHost) + throws IOException { // Get the path to the java executable from the current JVM String pathSeparator = System.getProperty("path.separator"); @@ -89,16 +93,19 @@ private static Pair createProcess(int port, boolean https) thr .filter(p -> p.contains("solr") && p.contains("core") && p.contains("build")) .collect(Collectors.joining(pathSeparator)); - ProcessBuilder processBuilder = - new ProcessBuilder( - System.getProperty("java.home") + "/bin/java", - "-Dsolr.port.listen=" + port, - "-DisHttps=" + https, - "-DmockSolr=true", - "-cp", - classPath, - "org.apache.solr.cli.SolrProcessManagerTest$MockSolrProcess", - https ? "--module=https" : "--module=http"); + List command = new ArrayList<>(); + command.add(System.getProperty("java.home") + "/bin/java"); + command.add("-Dsolr.port.listen=" + port); + command.add("-DisHttps=" + https); + command.add("-DmockSolr=true"); + if (bindHost != null) { + command.add("-Dsolr.host.bind=" + bindHost); + } + command.add("-cp"); + command.add(classPath); + command.add("org.apache.solr.cli.SolrProcessManagerTest$MockSolrProcess"); + command.add(https ? "--module=https" : "--module=http"); + ProcessBuilder processBuilder = new ProcessBuilder(command); // Start the process and read first line of output Process process = processBuilder.start(); @@ -112,13 +119,25 @@ private static Pair createProcess(int port, boolean https) thr public void testGetLocalUrl() { assertFalse(solrProcessManager.getAllRunning().isEmpty()); - solrProcessManager - .getAllRunning() - .forEach( - p -> - assertEquals( - (p.isHttps() ? "https" : "http") + "://localhost:" + p.port() + "/solr", - p.getLocalUrl())); + SolrProcess http = solrProcessManager.processForPort(processHttp.getKey()).orElseThrow(); + assertEquals("http://localhost:" + http.port() + "/solr", http.getLocalUrl()); + SolrProcess https = solrProcessManager.processForPort(processHttps.getKey()).orElseThrow(); + assertEquals("https://localhost:" + https.port() + "/solr", https.getLocalUrl()); + } + + public void testLocalConnectHost() { + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.empty())); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of(""))); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("0.0.0.0"))); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("::"))); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("127.0.0.1"))); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("::1"))); + assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("localhost"))); + assertEquals("10.0.0.5", SolrProcessManager.localConnectHost(Optional.of("10.0.0.5"))); + assertEquals( + "myhost.example.com", + SolrProcessManager.localConnectHost(Optional.of("myhost.example.com"))); + assertEquals("[fe80::1]", SolrProcessManager.localConnectHost(Optional.of("fe80::1"))); } public void testIsRunningWithPort() { From 80bac134ba658886306de91efb68e75eead6d94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 5 Aug 2026 15:56:01 +0200 Subject: [PATCH 3/5] Changelog --- .../unreleased/SOLR-18336-windows-start-wait.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog/unreleased/SOLR-18336-windows-start-wait.yml diff --git a/changelog/unreleased/SOLR-18336-windows-start-wait.yml b/changelog/unreleased/SOLR-18336-windows-start-wait.yml new file mode 100644 index 000000000000..6489d0a5c831 --- /dev/null +++ b/changelog/unreleased/SOLR-18336-windows-start-wait.yml @@ -0,0 +1,12 @@ +title: > + Fixed bin/solr.cmd start on Windows waiting for all running Solr instances + instead of only the one being started. Also fixed the wait to use the + configured bind host instead of always localhost. +type: fixed +authors: + - name: Jan Høydahl + url: https://home.apache.org/phonebook.html?uid=janhoy + - name: Mikael Sterner +links: + - name: SOLR-18336 + url: https://issues.apache.org/jira/browse/SOLR-18336 From a3b24ed46f70bd9a1b9cd4084b19584442cec223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 5 Aug 2026 16:54:20 +0200 Subject: [PATCH 4/5] SOLR-18336: Prefer advertised host and retry process scan in start wait The status tool's -p wait now re-scans for the process until the deadline instead of failing if the newly started process is not yet visible. The local URL for a process now prefers -Dsolr.host.advertise when set, falling back to the bind host, matching pre-10 behavior for non-loopback binds with SSL certificates issued for the advertised hostname. --- .../SOLR-18336-windows-start-wait.yml | 4 +- .../apache/solr/cli/SolrProcessManager.java | 32 +++++--- .../java/org/apache/solr/cli/StatusTool.java | 8 ++ .../solr/cli/SolrProcessManagerTest.java | 78 +++++++++++++------ 4 files changed, 86 insertions(+), 36 deletions(-) diff --git a/changelog/unreleased/SOLR-18336-windows-start-wait.yml b/changelog/unreleased/SOLR-18336-windows-start-wait.yml index 6489d0a5c831..5dfca0d8a1cf 100644 --- a/changelog/unreleased/SOLR-18336-windows-start-wait.yml +++ b/changelog/unreleased/SOLR-18336-windows-start-wait.yml @@ -1,7 +1,7 @@ title: > Fixed bin/solr.cmd start on Windows waiting for all running Solr instances - instead of only the one being started. Also fixed the wait to use the - configured bind host instead of always localhost. + instead of only the one being started. Also fixed the wait to connect using + the advertised or bind host instead of always localhost. type: fixed authors: - name: Jan Høydahl diff --git a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java index 8052cbf07ea9..46bf1ce55b10 100644 --- a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java @@ -76,7 +76,8 @@ public SolrProcessManager() { ph.pid(), parsePortFromProcess(ph).orElseThrow(), isProcessSsl(ph), - localConnectHost(parseBindHostFromProcess(ph))))); + localConnectHost( + parseAdvertiseHostFromProcess(ph), parseBindHostFromProcess(ph))))); portProcessMap = pidProcessMap.values().stream().collect(Collectors.toUnmodifiableMap(p -> p.port, p -> p)); String solrInstallDir = EnvUtils.getProperty(SOLR_INSTALL_DIR); @@ -162,15 +163,26 @@ private Optional parseBindHostFromProcess(ProcessHandle ph) { .findFirst(); } + private Optional parseAdvertiseHostFromProcess(ProcessHandle ph) { + return arguments(ph).stream() + .filter(a -> a.contains("-Dsolr.host.advertise=")) + .map(s -> s.split("=", 2)[1]) + .findFirst(); + } + /** - * Resolves the host to use when connecting locally to a Solr process: the host the process is - * bound to if it is a specific non-loopback address, otherwise {@code localhost}. Wildcard and - * loopback binds are reachable as {@code localhost}, which also keeps SSL hostname verification - * working with certificates issued for {@code localhost}. IPv6 literals are bracketed for use in - * URLs. + * Resolves the host to use when connecting locally to a Solr process. The advertised host is + * preferred when set, as that is the name the node is reachable by and, with SSL, the name its + * certificate is issued for. Otherwise the bind host is used if it is a specific non-loopback + * address. Wildcard and loopback binds are reachable as {@code localhost}. IPv6 literals are + * bracketed for use in URLs. */ - static String localConnectHost(Optional bindHost) { - String host = bindHost.map(String::trim).orElse(""); + static String localConnectHost(Optional advertiseHost, Optional bindHost) { + String host = + advertiseHost + .map(String::trim) + .filter(h -> !h.isEmpty()) + .orElseGet(() -> bindHost.map(String::trim).orElse("")); return switch (host) { case "", "0.0.0.0", "::", "[::]", "127.0.0.1", "::1", "[::1]", "localhost" -> "localhost"; default -> host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; @@ -264,8 +276,8 @@ private static List arguments(ProcessHandle ph) { /** * Represents a running Solr process. The {@code host} is the host to use when connecting to the - * process from the local machine, i.e. the bind host if bound to a specific address, else {@code - * localhost}. + * process from the local machine, i.e. the advertised host if set, else the bind host if bound to + * a specific address, else {@code localhost}. */ public record SolrProcess(long pid, int port, boolean isHttps, String host) { diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 228908d8fb30..043b63f3c527 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -99,6 +99,7 @@ public Options getOptions() { } @Override + @SuppressWarnings("BusyWait") public void runImpl(CommandLine cli) throws Exception { String solrUrl = CLIUtils.hasConnectionOption(cli) ? CLIUtils.normalizeSolrUrl(cli) : null; Integer port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; @@ -128,6 +129,13 @@ public void runImpl(CommandLine cli) throws Exception { if (port != null) { Optional proc = processMgr.processForPort(port); + // A newly started process may not be visible in the process table yet, so when asked to + // wait, keep re-scanning until the deadline rather than failing on the first miss + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxWaitSecs); + while (proc.isEmpty() && System.nanoTime() < deadline) { + Thread.sleep(1000L); + proc = new SolrProcessManager().processForPort(port); + } if (proc.isEmpty()) { CLIO.err("Could not find a running Solr on port " + port); runtime.exit(1); diff --git a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java index b95b57d9a02b..7512a685936f 100644 --- a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java +++ b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java @@ -47,24 +47,30 @@ public class SolrProcessManagerTest extends SolrTestCase { private static SolrProcessManager solrProcessManager; private static Pair processHttp; private static Pair processHttps; + private static Pair processBoundHttp; + private static Pair processAdvertisedHttps; @BeforeClass public static void beforeClass() throws Exception { boolean isWindows = random().nextBoolean(); String PID_SUFFIX = isWindows ? ".port" : ".pid"; log.info("Simulating pid file on {}", isWindows ? "Windows" : "Linux"); - processHttp = createProcess(findAvailablePort(), false, null); - processHttps = createProcess(findAvailablePort(), true, "127.0.0.1"); - long processHttpValue = isWindows ? processHttp.getKey() : processHttp.getValue().pid(); - long processHttpsValue = isWindows ? processHttps.getKey() : processHttps.getValue().pid(); + processHttp = createProcess(findAvailablePort(), false, null, null); + processHttps = createProcess(findAvailablePort(), true, "127.0.0.1", null); + // The mock process does not actually bind to these hosts, they are only command-line markers + processBoundHttp = createProcess(findAvailablePort(), false, "10.99.99.99", null); + processAdvertisedHttps = + createProcess(findAvailablePort(), true, "10.99.99.99", "myhost.example.com"); SolrProcessManager.enableTestingMode = true; System.setProperty("solr.port.listen", Integer.toString(processHttp.getKey())); Path pidDir = createTempDir("solr-pid-dir"); System.setProperty("solr.pid.dir", pidDir.toString()); - Files.writeString( - pidDir.resolve("solr-" + processHttpValue + PID_SUFFIX), Long.toString(processHttpValue)); - Files.writeString( - pidDir.resolve("solr-" + processHttpsValue + PID_SUFFIX), Long.toString(processHttpsValue)); + for (Pair p : + List.of(processHttp, processHttps, processBoundHttp, processAdvertisedHttps)) { + long pidFileValue = isWindows ? p.getKey() : p.getValue().pid(); + Files.writeString( + pidDir.resolve("solr-" + pidFileValue + PID_SUFFIX), Long.toString(pidFileValue)); + } Files.writeString(pidDir.resolve("solr-99999" + PID_SUFFIX), "99999"); // Invalid solrProcessManager = new SolrProcessManager(); } @@ -73,6 +79,8 @@ public static void beforeClass() throws Exception { public static void afterClass() throws Exception { processHttp.getValue().destroyForcibly(); processHttps.getValue().destroyForcibly(); + processBoundHttp.getValue().destroyForcibly(); + processAdvertisedHttps.getValue().destroyForcibly(); SolrProcessManager.enableTestingMode = false; } @@ -83,8 +91,8 @@ private static int findAvailablePort() throws IOException { } @SuppressWarnings("SystemGetProperty") - private static Pair createProcess(int port, boolean https, String bindHost) - throws IOException { + private static Pair createProcess( + int port, boolean https, String bindHost, String advertiseHost) throws IOException { // Get the path to the java executable from the current JVM String pathSeparator = System.getProperty("path.separator"); @@ -101,6 +109,9 @@ private static Pair createProcess(int port, boolean https, Str if (bindHost != null) { command.add("-Dsolr.host.bind=" + bindHost); } + if (advertiseHost != null) { + command.add("-Dsolr.host.advertise=" + advertiseHost); + } command.add("-cp"); command.add(classPath); command.add("org.apache.solr.cli.SolrProcessManagerTest$MockSolrProcess"); @@ -123,21 +134,40 @@ public void testGetLocalUrl() { assertEquals("http://localhost:" + http.port() + "/solr", http.getLocalUrl()); SolrProcess https = solrProcessManager.processForPort(processHttps.getKey()).orElseThrow(); assertEquals("https://localhost:" + https.port() + "/solr", https.getLocalUrl()); + // Non-loopback bind host is used for the local URL + SolrProcess bound = solrProcessManager.processForPort(processBoundHttp.getKey()).orElseThrow(); + assertEquals("http://10.99.99.99:" + bound.port() + "/solr", bound.getLocalUrl()); + // Advertised host wins over the bind host + SolrProcess advertised = + solrProcessManager.processForPort(processAdvertisedHttps.getKey()).orElseThrow(); + assertEquals( + "https://myhost.example.com:" + advertised.port() + "/solr", advertised.getLocalUrl()); } public void testLocalConnectHost() { - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.empty())); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of(""))); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("0.0.0.0"))); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("::"))); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("127.0.0.1"))); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("::1"))); - assertEquals("localhost", SolrProcessManager.localConnectHost(Optional.of("localhost"))); - assertEquals("10.0.0.5", SolrProcessManager.localConnectHost(Optional.of("10.0.0.5"))); - assertEquals( - "myhost.example.com", - SolrProcessManager.localConnectHost(Optional.of("myhost.example.com"))); - assertEquals("[fe80::1]", SolrProcessManager.localConnectHost(Optional.of("fe80::1"))); + // No advertise host: bind host decides, loopback and wildcard binds map to localhost + assertEquals("localhost", localConnectHost(null, null)); + assertEquals("localhost", localConnectHost(null, "")); + assertEquals("localhost", localConnectHost(null, "0.0.0.0")); + assertEquals("localhost", localConnectHost(null, "::")); + assertEquals("localhost", localConnectHost(null, "127.0.0.1")); + assertEquals("localhost", localConnectHost(null, "::1")); + assertEquals("localhost", localConnectHost(null, "localhost")); + assertEquals("10.0.0.5", localConnectHost(null, "10.0.0.5")); + assertEquals("myhost.example.com", localConnectHost(null, "myhost.example.com")); + assertEquals("[fe80::1]", localConnectHost(null, "fe80::1")); + // Advertise host is preferred over the bind host when set + assertEquals("myhost.example.com", localConnectHost("myhost.example.com", "10.0.0.5")); + assertEquals("myhost.example.com", localConnectHost("myhost.example.com", null)); + assertEquals("localhost", localConnectHost("localhost", "10.0.0.5")); + // Blank advertise host falls back to the bind host + assertEquals("10.0.0.5", localConnectHost("", "10.0.0.5")); + assertEquals("10.0.0.5", localConnectHost(" ", "10.0.0.5")); + } + + private static String localConnectHost(String advertiseHost, String bindHost) { + return SolrProcessManager.localConnectHost( + Optional.ofNullable(advertiseHost), Optional.ofNullable(bindHost)); } public void testIsRunningWithPort() { @@ -172,12 +202,12 @@ public void testGetProcessForPid() { public void testScanSolrPidFiles() throws IOException { Collection processes = solrProcessManager.scanSolrPidFiles(); - assertEquals(2, processes.size()); + assertEquals(4, processes.size()); } public void testGetAllRunning() { Collection processes = solrProcessManager.getAllRunning(); - assertEquals(2, processes.size()); + assertEquals(4, processes.size()); } public void testSolrProcessMethods() { From 027bf39c48b49e29807036591a2668e0ee3493ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 5 Aug 2026 17:01:58 +0200 Subject: [PATCH 5/5] SOLR-18336: Simplify process arg parsing and extract waitForProcessOnPort Collapse three near-identical sysprop-parsing pipelines into one helper, dropping the unreachable -Dsolr.jetty.host fallback (10.x scripts always translate the deprecated name to solr.host.bind before launch). Move the process wait loop into SolrProcessManager.waitForProcessOnPort, reusing the TimeOut utility. Start mock test processes in parallel. --- .../apache/solr/cli/SolrProcessManager.java | 43 +++++++++++-------- .../java/org/apache/solr/cli/StatusTool.java | 13 ++---- .../solr/cli/SolrProcessManagerTest.java | 17 ++++++-- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java index 46bf1ce55b10..fe4322a80714 100644 --- a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,6 +42,8 @@ import org.apache.lucene.util.Constants; import org.apache.solr.common.SolrException; import org.apache.solr.common.util.EnvUtils; +import org.apache.solr.common.util.TimeSource; +import org.apache.solr.util.TimeOut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,10 +77,13 @@ public SolrProcessManager() { ph -> new SolrProcess( ph.pid(), - parsePortFromProcess(ph).orElseThrow(), + parseSyspropFromProcess(ph, "solr.port.listen") + .map(Integer::parseInt) + .orElseThrow(), isProcessSsl(ph), localConnectHost( - parseAdvertiseHostFromProcess(ph), parseBindHostFromProcess(ph))))); + parseSyspropFromProcess(ph, "solr.host.advertise"), + parseSyspropFromProcess(ph, "solr.host.bind"))))); portProcessMap = pidProcessMap.values().stream().collect(Collectors.toUnmodifiableMap(p -> p.port, p -> p)); String solrInstallDir = EnvUtils.getProperty(SOLR_INSTALL_DIR); @@ -147,27 +153,28 @@ public Collection getAllRunning() { return pidProcessMap.values(); } - private Optional parsePortFromProcess(ProcessHandle ph) { - Optional portStr = - arguments(ph).stream() - .filter(a -> a.contains("-Dsolr.port.listen=")) - .map(s -> s.split("=")[1]) - .findFirst(); - return portStr.isPresent() ? portStr.map(Integer::parseInt) : Optional.empty(); - } - - private Optional parseBindHostFromProcess(ProcessHandle ph) { + /** Parses the value of the given system property from the process' command line arguments */ + private static Optional parseSyspropFromProcess(ProcessHandle ph, String sysprop) { return arguments(ph).stream() - .filter(a -> a.contains("-Dsolr.host.bind=") || a.contains("-Dsolr.jetty.host=")) + .filter(a -> a.contains("-D" + sysprop + "=")) .map(s -> s.split("=", 2)[1]) .findFirst(); } - private Optional parseAdvertiseHostFromProcess(ProcessHandle ph) { - return arguments(ph).stream() - .filter(a -> a.contains("-Dsolr.host.advertise=")) - .map(s -> s.split("=", 2)[1]) - .findFirst(); + /** + * Returns the process listening on the given port, if found, waiting up to {@code maxWaitSecs} + * for it to appear. A newly started process may not be visible in the process table right away, + * so the table is re-scanned once a second until the deadline. + */ + public Optional waitForProcessOnPort(int port, int maxWaitSecs) + throws InterruptedException { + Optional proc = processForPort(port); + TimeOut timeOut = new TimeOut(maxWaitSecs, TimeUnit.SECONDS, TimeSource.NANO_TIME); + while (proc.isEmpty() && !timeOut.hasTimedOut()) { + timeOut.sleep(1000); + proc = new SolrProcessManager().processForPort(port); + } + return proc; } /** diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 043b63f3c527..be7ae6755057 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -62,7 +62,7 @@ public class StatusTool extends ToolBase { .hasArg() .argName("PORT") .type(Integer.class) - .desc("Port on localhost to check status for") + .desc("Port of a local Solr process to check status for") .get(); public static final Option SHORT_OPTION = @@ -99,7 +99,6 @@ public Options getOptions() { } @Override - @SuppressWarnings("BusyWait") public void runImpl(CommandLine cli) throws Exception { String solrUrl = CLIUtils.hasConnectionOption(cli) ? CLIUtils.normalizeSolrUrl(cli) : null; Integer port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; @@ -128,14 +127,8 @@ public void runImpl(CommandLine cli) throws Exception { } if (port != null) { - Optional proc = processMgr.processForPort(port); - // A newly started process may not be visible in the process table yet, so when asked to - // wait, keep re-scanning until the deadline rather than failing on the first miss - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxWaitSecs); - while (proc.isEmpty() && System.nanoTime() < deadline) { - Thread.sleep(1000L); - proc = new SolrProcessManager().processForPort(port); - } + // When asked to wait, this also waits for a newly started process to become visible + Optional proc = processMgr.waitForProcessOnPort(port, maxWaitSecs); if (proc.isEmpty()) { CLIO.err("Could not find a running Solr on port " + port); runtime.exit(1); diff --git a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java index 7512a685936f..1e428e900454 100644 --- a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java +++ b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java @@ -61,6 +61,10 @@ public static void beforeClass() throws Exception { processBoundHttp = createProcess(findAvailablePort(), false, "10.99.99.99", null); processAdvertisedHttps = createProcess(findAvailablePort(), true, "10.99.99.99", "myhost.example.com"); + for (Pair p : + List.of(processHttp, processHttps, processBoundHttp, processAdvertisedHttps)) { + awaitReady(p.getValue()); + } SolrProcessManager.enableTestingMode = true; System.setProperty("solr.port.listen", Integer.toString(processHttp.getKey())); Path pidDir = createTempDir("solr-pid-dir"); @@ -116,16 +120,16 @@ private static Pair createProcess( command.add(classPath); command.add("org.apache.solr.cli.SolrProcessManagerTest$MockSolrProcess"); command.add(https ? "--module=https" : "--module=http"); - ProcessBuilder processBuilder = new ProcessBuilder(command); + return new Pair<>(port, new ProcessBuilder(command).start()); + } - // Start the process and read first line of output - Process process = processBuilder.start(); + /** Waits for the mock process to print its ready line, so the processes can start in parallel */ + private static void awaitReady(Process process) throws IOException { try (InputStream is = process.getInputStream(); InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { System.out.println(br.readLine()); } - return new Pair<>(port, process); } public void testGetLocalUrl() { @@ -170,6 +174,11 @@ private static String localConnectHost(String advertiseHost, String bindHost) { Optional.ofNullable(advertiseHost), Optional.ofNullable(bindHost)); } + public void testWaitForProcessOnPort() throws Exception { + assertTrue(solrProcessManager.waitForProcessOnPort(processHttp.getKey(), 0).isPresent()); + assertTrue(solrProcessManager.waitForProcessOnPort(0, 0).isEmpty()); + } + public void testIsRunningWithPort() { assertFalse(solrProcessManager.isRunningWithPort(0)); assertTrue(solrProcessManager.isRunningWithPort(processHttp.getKey()));