Fix: OpenSearch process does not exit when startup fails due to StartupException - #22259
Conversation
PR Reviewer Guide 🔍(Review updated until commit 2c7bb2b)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to d270b56 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 07ac748
Suggestions up to commit 7bfb1fb
Suggestions up to commit 23ea016
Suggestions up to commit 61cf667
Suggestions up to commit 84c3696
|
|
❌ Gradle check result for ee5004b: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit d669f14 |
|
❌ Gradle check result for d669f14: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit c6438ee |
|
Persistent review updated to latest commit 8a930f0 |
|
Persistent review updated to latest commit 115ca81 |
|
❌ Gradle check result for 115ca81: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
115ca81 to
8a930f0
Compare
|
Persistent review updated to latest commit 8a930f0 |
|
❌ Gradle check result for 8a930f0: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit 0a86cbc |
|
❌ Gradle check result for 0a86cbc: null Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
| 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; | ||
| } |
There was a problem hiding this comment.
[Nitpick] Would it make sense to do something more like:
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);
status = ExitCodes.CODE_ERROR;
// Continue to the next if-statement to exit().
}
What do you think? It's essentially the same thing (and the same number of lines), but part of me likes having exactly one place where we call exit().
I guess another option would be to modify OpenSearch.main(String[], OpenSearch, Terminal) to be:
static int main(final String[] args, final OpenSearch opensearch, final Terminal terminal) throws Exception {
try {
return opensearch.main(args, terminal);
} catch (StartupException e) {
e.printStackTrace(terminal.getErrorWriter());
return ExitCodes.CODE_ERROR;
}
}
Incidentally, I think the e.printStackTrace(System.err) in your solution should at least be e.printStackTrace(Terminal.DEFAULT.getErrorWriter()). I just read up on System.console() (since the ConsoleTerminal is the one that differentiates from SystemTerminal, which just delegates to System.out and System.err), since I was unfamiliar with it. I think the essential piece is that Console synchronizes its output (and input) methods, so at least a println is guaranteed to be atomic. (I guess lines of a stack trace could get interleaved with other lines.) It looks like localization may also be affected (if the console's locale differs from the system's locale).
There was a problem hiding this comment.
went with option 2. moved the catch into the 3-arg main so it uses the injected terminal instead of Terminal.DEFAULT and main(String[]) keeps a single exit() call.
Didn't know about the Console sync and locale difference between System.err and `terminal.getErrorWriter() . Good to know
|
Persistent review updated to latest commit 7677bcf |
|
❌ Gradle check result for 7677bcf: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit d8b7bd5 |
d8b7bd5 to
84c3696
Compare
|
Persistent review updated to latest commit 84c3696 |
…tartup failure 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 <aparajita31pandey@gmail.com>
…Exception 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 <aparajita31pandey@gmail.com>
… on startup failure 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 <aparajita31pandey@gmail.com>
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 <aparajita31pandey@gmail.com>
9eb3606 to
61cf667
Compare
|
Persistent review updated to latest commit 61cf667 |
Signed-off-by: Aparajita Pandey <aparajita31pandey@gmail.com>
|
Persistent review updated to latest commit 23ea016 |
|
❌ Gradle check result for 23ea016: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
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 <aparajita31pandey@gmail.com>
887db9c to
7bfb1fb
Compare
|
Persistent review updated to latest commit 7bfb1fb |
7bfb1fb to
07ac748
Compare
|
Persistent review updated to latest commit 07ac748 |
|
❌ Gradle check result for 07ac748: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
Signed-off-by: Aparajita Pandey <aparajita31pandey@gmail.com>
|
Persistent review updated to latest commit d270b56 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #22259 +/- ##
============================================
+ Coverage 73.43% 73.52% +0.08%
- Complexity 76472 76545 +73
============================================
Files 6104 6104
Lines 346573 346590 +17
Branches 49886 49888 +2
============================================
+ Hits 254514 254833 +319
+ Misses 71798 71499 -299
+ Partials 20261 20258 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Persistent review updated to latest commit 2c7bb2b |
|
❌ Gradle check result for 2c7bb2b: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
Description
When a plugin or bootstrap component can throws a
RuntimeExceptionduring startup,OpenSearch.init()wraps it in aStartupExceptionand rethrows it.StartupExceptionwas never caught in the call chain — it escapedOpenSearch.main(String[])entirely, bypassing theexit(status)call.This lefts the JVM process hanging indefinitely after startup failure.
Resolves
#22260
Root Cause
StartupExceptionis aRuntimeException. The CLI framework inCommand.main()only handlesOptionExceptionandUserException— soStartupExceptionpropagates uncaught all the way throughmain(String[], OpenSearch, Terminal)and escapesmain(String[]). When non-daemon threads are still alive (as they are during a partial bootstrap), the JVM does not terminate automatically, leaving the process hanging.Fix
StartupExceptiondesigned to escape tomain()— itsprintStackTrace()override has a comment: "This logic actually prints the exception to the console, its what is invoked by the JVM when we throw the exception from main()". The formatted output was already correct. The only missing piece wasexit().Catch
StartupExceptioninmain(String[])— the correct level whereSystem.erris appropriate and process exit decisions belong — calle.printStackTrace(System.err)to preserve the existing custom-formatted output (which truncates guice frames, etc.), thenexit(CODE_ERROR):Stack trace (reproducer)
Signed-off-by: Aparajita Pandey aparajita31pandey@gmail.com