Skip to content

fix(suspendapp): avoid deadlock and align JVM behaviour on system.exit#3930

Open
tKe wants to merge 2 commits into
mainfrom
tKe/suspend-app-system-exit
Open

fix(suspendapp): avoid deadlock and align JVM behaviour on system.exit#3930
tKe wants to merge 2 commits into
mainfrom
tKe/suspend-app-system-exit

Conversation

@tKe

@tKe tKe commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

fixes #3846

This fix essentially sidesteps the deadlock by not attempting to cancel/graceful-shutdown when the hook is invoked for any reason other than SIGINT/SIGTERM.

The rationale is that System.exit equivalents on other platforms doesn't trigger cancellation of the SuspendApp, and the other hook invocation cause (last non-daemon thread terminated) is not expected through the normal usage of SuspendApp (that is, as the entry point on the main thread for a service/application).

Instead, this fixes the Signal handling, captures the signal, and limits triggering cancellation to only when signalled.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Kover Report

File Coverage [0.00%]
arrow-libs/suspendapp/suspendapp/src/jvmMain/kotlin/arrow/continuations/Enviroment.jvm.kt 0.00%
Total Project Coverage 48.25%

@tKe
tKe requested review from nomisRev and serras June 27, 2026 09:11
@serras

serras commented Jun 27, 2026

Copy link
Copy Markdown
Member

@tKe could you rebase after the merging of #3917? In fact, is there any relation between both PRs that need to be taken into account?

@tKe

tKe commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

@serras rebasing this, which includes job name fixes for the js tests, highlighted that the exitApp change has introduced a regression in then wasm/js signal handling as it now reports an exitCode of 255 not 128+sig. I'll include a fix in this rebase.

@tKe
tKe force-pushed the tKe/suspend-app-system-exit branch from 2272659 to 3b13f57 Compare June 27, 2026 10:48
@tKe
tKe requested a review from serras June 27, 2026 18:08

@nomisRev nomisRev Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps just e.printStackTrace() here? I guess this is what was swallowing the IllegalArgumentException that was thrown for using SIGINT vs just INT.

Alternatively we do not catch it at all and just let it throw?

@tKe tKe Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the invocation of Signal(signal) that throws when the signal is unknown.
The Signal.handle invocation may also throw if the signal is reserved by the VM/OS.
It might be safer to e.printStackTrace or add a custom log warning that signal handling for SuspendApp may be impacted. Otherwise the cases where we don't cancel on signal as we haven't installed the handler would be hard to diagnose.

Runtime.getRuntime().removeShutdownHook(hook)
try {
Runtime.getRuntime().removeShutdownHook(hook)
} catch (_: IllegalStateException) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this occur? It should be gated by isShutdown, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlikely though it is, there's the possibility the runtime shutdown process has been started but our shutdown hook thread has not yet started or set the isShutdown flag. Attempting to remove hooks once the shutdown process has started will fail.

Signal.handle(Signal(signal)) { prev ->
runBlocking { action() }
Signal.handle(Signal(signal)) { sig ->
runBlocking { action(sig.number) }

@nomisRev nomisRev Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violates the "exit code formula" Exit Code=128+Signal Number.

The exit code formula Exit Code=128+Signal Number is not being respected here since now we set 2 for SIGINT, and 15 for SIGTERM. This seems consistently implemented in the other platforms.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with the other platforms where the onSigInt/onSigTerm blocks receive the signal number (i.e. 2 / 15). We could name the parameter of the lambda for clarity and avoid using unqualified code in most places but I didn't want to make too many tweaks outside of the main area of impact.

The other platforms handlers tend to do the sig+128 maths to convert an exit code in the block passed to onSigInt/onSigTerm.
i.e. { code -> exitAfter(128 + code) { block() } }

For the JVM, we let the JVM handle the exit code calculation based on the default signal handler so we needn't do the calculation here (although we could theoretically call System.halt we don't know if other shutdown hooks have finished or not, nor have control of the ordering)

delay(0.5.seconds)
sendSignal(Signal.SIGTERM)
}
// TODO: inconsistent exit codes across platforms, for now just check it's not a success

@nomisRev nomisRev Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like an inconsistency introduced in this PR? See other comment https://github.com/arrow-kt/arrow/pull/3930/changes#r3519183429.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I believe the inconsistency is on JVM still exiting with the correct signalled exit code (128+sig) but the other platforms are exiting with exit code 255 due to the timeout exception.

JvmSpec:waitAndTimeout exitCode == 143
JsSpec:waitAndTimeout exitCode == 255
WasmJsSpec:waitAndTimeout exitCode == 255
NativeSpec:waitAndTimeout exitCode == 255

@nomisRev

nomisRev commented Jul 3, 2026

Copy link
Copy Markdown
Member

@tKe couple of small questions and remarks. Feels like a relatively large change, but it mostly tries to prevent hanging indefinitely which I guess is okay. What is a bit strange to me is that now silently in those cases the shutdown will not happen.

Tagging @alexandru since he recently also worked here and is familiar with semantics of shutdown and would like to hear his thoughts if he has time to chime in.

@tKe

tKe commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review @nomisRev. I've addressed or replied to your feedback.

It is a large change, but ultimately the shutdown hook not cancelling the app on exitProcess is aligned with the other platforms doing the same, and the new exitApp from @alexandru provides an alternative nicely.

It also aligns the platform handling of SIGHUP which will currently terminate without cancellation/shutdown on JS/Wasm/Native and JVM now (whereas before it would cancel on JVM).

Only SIGQUIT differs amongst the other platforms, with the JVM dumping stacktrace but otherwise continuing (expected JVM functionality), and the other platforms exiting without cancellation with exit code 131.

Otherwise the change only impacts when SuspendApp isn't on a non-daemon thread (which would be very unexpected usage) as it would otherwise have unregistered the shutdown hook before it would be invoked due to last non-daemon thread terminating.

For the expected usage patterns that aren't calling exitProcess (which previously deadlocked and never shutdown gracefully anyway), the impact is that we no longer cancel on SIGHUP.

@tKe
tKe requested a review from nomisRev July 7, 2026 11:01
@serras

serras commented Jul 15, 2026

Copy link
Copy Markdown
Member

@nomisRev are you OK with the reviewed code? I’d like to merge this one, since it seems it fixes an important problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] SuspendApp freezes app on System.exit

3 participants