From 0b835cbeee50b56d558292e31ce870db36b13569 Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 21 Jun 2026 12:48:45 +0530 Subject: [PATCH 1/7] Fix: catch StartupException in execute() to ensure process exits on startup failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a plugin or bootstrap component throws a RuntimeException during startup, OpenSearch.init() wraps it in a StartupException. Previously, StartupException propagated uncaught through execute() and escaped OpenSearch.main(String[]) entirely, bypassing the exit(status) call. This left the JVM process hanging — especially problematic in Docker/k8s where a non-zero exit is required for the orchestrator to detect failure. Fix: catch StartupException in execute() alongside the existing NodeValidationException handler and rethrow it as UserException with ExitCodes.CODE_ERROR so the CLI framework returns a non-OK status and exit(status) is called. Added a regression test in OpenSearchCliTests that simulates a StartupException thrown from init() and asserts ExitCodes.CODE_ERROR is returned. Signed-off-by: Aparajita Pandey --- .../java/org/opensearch/bootstrap/OpenSearch.java | 2 ++ .../org/opensearch/bootstrap/OpenSearchCliTests.java | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java index 7b011b5828428..052813b3a43a4 100644 --- a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java +++ b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java @@ -159,6 +159,8 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th init(daemonize, pidFile, quiet, env); } catch (NodeValidationException e) { throw new UserException(ExitCodes.CONFIG, e.getMessage()); + } catch (StartupException e) { + throw new UserException(ExitCodes.CODE_ERROR, e.getMessage()); } } diff --git a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java index f4d8e055f0795..45e50233afa49 100644 --- a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java @@ -226,4 +226,16 @@ public void testUnknownOption() throws Exception { ); } + public void testStartupExceptionExitsWithCodeError() throws Exception { + // Verify that a StartupException thrown during bootstrap causes a non-OK exit rather than + // leaving the process hanging (the exception must be caught and converted to UserException). + final RuntimeException cause = new RuntimeException("dummy startup failure"); + runTest( + ExitCodes.CODE_ERROR, + true, + (output, error) -> assertThat(error, containsString("dummy startup failure")), + (foreground, pidFile, quiet, env) -> { throw new StartupException(cause); } + ); + } + } From 082c9ba68ab4385826e999e23820d245c715c3db Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 21 Jun 2026 14:03:25 +0530 Subject: [PATCH 2/7] Preserve exception cause chain when wrapping StartupException as UserException Pass the original StartupException as the cause to UserException so the full stack trace is retained for debugging, per reviewer suggestion. Signed-off-by: Aparajita Pandey --- server/src/main/java/org/opensearch/bootstrap/OpenSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java index 052813b3a43a4..3f781786795dc 100644 --- a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java +++ b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java @@ -160,7 +160,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } catch (NodeValidationException e) { throw new UserException(ExitCodes.CONFIG, e.getMessage()); } catch (StartupException e) { - throw new UserException(ExitCodes.CODE_ERROR, e.getMessage()); + throw new UserException(ExitCodes.CODE_ERROR, e.getMessage(), e); } } From 29ffedf11e24bd42bacf545d9fea992fdb2324ad Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 21 Jun 2026 14:50:42 +0530 Subject: [PATCH 3/7] Fix: catch StartupException in main(String[]) to ensure process exits on startup failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StartupException was designed to escape to main() and be printed via its custom printStackTrace formatter (truncates guice frames, etc.). The bug was that exit() was never called after it escaped, leaving the JVM process hanging when non-daemon threads were still alive. Fix: catch StartupException in main(String[]) — the correct level where System.err is appropriate and process exit decisions belong — call e.printStackTrace(System.err) to preserve the existing formatted output, then exit(CODE_ERROR). The execute() catch is reverted: StartupException is not a UserException and should not flow through the CLI error path. Test updated to assert StartupException propagates through the 3-arg main (test harness path) with the original cause preserved. Signed-off-by: Aparajita Pandey --- .../org/opensearch/bootstrap/OpenSearch.java | 14 +++++++++++--- .../opensearch/bootstrap/OpenSearchCliTests.java | 16 +++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java index 3f781786795dc..52407efc6d616 100644 --- a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java +++ b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java @@ -88,7 +88,17 @@ public static void main(final String[] args) throws Exception { LogConfigurator.registerErrorListener(); final OpenSearch opensearch = new OpenSearch(); - int status = main(args, opensearch, Terminal.DEFAULT); + int status; + try { + status = main(args, opensearch, Terminal.DEFAULT); + } catch (StartupException e) { + // StartupException has custom printStackTrace formatting (truncates guice frames, etc.). + // Catch it here so the process exits rather than hanging, while preserving that output. + e.printStackTrace(System.err); + exit(ExitCodes.CODE_ERROR); + return; + } + if (status != ExitCodes.OK) { final String basePath = System.getProperty("opensearch.logs.base_path"); // It's possible to fail before logging has been configured, in which case there's no point @@ -159,8 +169,6 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th init(daemonize, pidFile, quiet, env); } catch (NodeValidationException e) { throw new UserException(ExitCodes.CONFIG, e.getMessage()); - } catch (StartupException e) { - throw new UserException(ExitCodes.CODE_ERROR, e.getMessage(), e); } } diff --git a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java index 45e50233afa49..4cfe9ad6fa08c 100644 --- a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java @@ -227,15 +227,17 @@ public void testUnknownOption() throws Exception { } public void testStartupExceptionExitsWithCodeError() throws Exception { - // Verify that a StartupException thrown during bootstrap causes a non-OK exit rather than - // leaving the process hanging (the exception must be caught and converted to UserException). + // StartupException must escape execute() and propagate to main(String[]) where it is caught, + // printed via its custom formatter, and exit(CODE_ERROR) is called. + // Via the test harness (3-arg main), it surfaces as an uncaught StartupException. final RuntimeException cause = new RuntimeException("dummy startup failure"); - runTest( - ExitCodes.CODE_ERROR, - true, - (output, error) -> assertThat(error, containsString("dummy startup failure")), - (foreground, pidFile, quiet, env) -> { throw new StartupException(cause); } + StartupException thrown = expectThrows( + StartupException.class, + () -> runTest(ExitCodes.OK, true, (output, error) -> {}, (foreground, pidFile, quiet, env) -> { + throw new StartupException(cause); + }) ); + assertSame(cause, thrown.getCause()); } } From 61cf6674548a22b1ced3e67c6e2d91c362fff726 Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 19 Jul 2026 19:36:25 +0530 Subject: [PATCH 4/7] Refactor: move StartupException catch to 3-arg main Move the StartupException handler from main(String[]) into main(String[], OpenSearch, Terminal) so that: - The terminal parameter is used directly (terminal.getErrorWriter()) instead of hardcoding Terminal.DEFAULT, making it testable. - main(String[]) retains a single exit() call path. Signed-off-by: Aparajita Pandey --- .../org/opensearch/bootstrap/OpenSearch.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java index 52407efc6d616..8e5f78642dd07 100644 --- a/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java +++ b/server/src/main/java/org/opensearch/bootstrap/OpenSearch.java @@ -88,16 +88,7 @@ public static void main(final String[] args) throws Exception { LogConfigurator.registerErrorListener(); final OpenSearch opensearch = new OpenSearch(); - int status; - try { - status = main(args, opensearch, Terminal.DEFAULT); - } catch (StartupException e) { - // StartupException has custom printStackTrace formatting (truncates guice frames, etc.). - // Catch it here so the process exits rather than hanging, while preserving that output. - e.printStackTrace(System.err); - exit(ExitCodes.CODE_ERROR); - return; - } + int status = main(args, opensearch, Terminal.DEFAULT); if (status != ExitCodes.OK) { final String basePath = System.getProperty("opensearch.logs.base_path"); @@ -132,7 +123,14 @@ private static void overrideDnsCachePolicyProperties() { } static int main(final String[] args, final OpenSearch opensearch, final Terminal terminal) throws Exception { - return opensearch.main(args, terminal); + try { + return opensearch.main(args, terminal); + } catch (StartupException e) { + // StartupException has custom printStackTrace formatting (truncates guice frames, etc.). + // Catch it here so the process exits rather than hanging, while preserving that output. + e.printStackTrace(terminal.getErrorWriter()); + return ExitCodes.CODE_ERROR; + } } @Override From 6901b8af9a82010075c0b26ed0996c062fbd2437 Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 19 Jul 2026 19:54:55 +0530 Subject: [PATCH 5/7] Trigger CI Signed-off-by: Aparajita Pandey From 07ac74886a6855653b758cff40ff64e5d43ebad0 Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 19 Jul 2026 20:40:55 +0530 Subject: [PATCH 6/7] Fix test to match Option 2 behavior of StartupException catch The 3-arg main() now catches StartupException and returns CODE_ERROR instead of propagating it. Update testStartupExceptionExitsWithCodeError to expect CODE_ERROR exit status and verify the cause message appears in error output, rather than expecting an uncaught StartupException. Signed-off-by: Aparajita Pandey --- .../opensearch/bootstrap/OpenSearchCliTests.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java index 4cfe9ad6fa08c..f4b838e65f196 100644 --- a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java @@ -227,17 +227,15 @@ public void testUnknownOption() throws Exception { } public void testStartupExceptionExitsWithCodeError() throws Exception { - // StartupException must escape execute() and propagate to main(String[]) where it is caught, - // printed via its custom formatter, and exit(CODE_ERROR) is called. - // Via the test harness (3-arg main), it surfaces as an uncaught StartupException. + // StartupException is caught in the 3-arg main(), which prints it via its custom formatter + // and returns CODE_ERROR so that the 1-arg main() calls exit(CODE_ERROR). final RuntimeException cause = new RuntimeException("dummy startup failure"); - StartupException thrown = expectThrows( - StartupException.class, - () -> runTest(ExitCodes.OK, true, (output, error) -> {}, (foreground, pidFile, quiet, env) -> { - throw new StartupException(cause); - }) + runTest( + ExitCodes.CODE_ERROR, + true, + (output, error) -> assertThat(error, containsString(cause.getMessage())), + (foreground, pidFile, quiet, env) -> { throw new StartupException(cause); } ); - assertSame(cause, thrown.getCause()); } } From d270b5675ecd8b3d1bc8662ec29d3cd21a7d0046 Mon Sep 17 00:00:00 2001 From: Aparajita Pandey Date: Sun, 19 Jul 2026 21:26:23 +0530 Subject: [PATCH 7/7] Apply spotless formatting to OpenSearchCliTests Signed-off-by: Aparajita Pandey --- .../java/org/opensearch/bootstrap/OpenSearchCliTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java index f4b838e65f196..481d2c14339bb 100644 --- a/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/OpenSearchCliTests.java @@ -234,7 +234,9 @@ public void testStartupExceptionExitsWithCodeError() throws Exception { ExitCodes.CODE_ERROR, true, (output, error) -> assertThat(error, containsString(cause.getMessage())), - (foreground, pidFile, quiet, env) -> { throw new StartupException(cause); } + (foreground, pidFile, quiet, env) -> { + throw new StartupException(cause); + } ); }