@@ -3,6 +3,7 @@ package ox
33import scala .util .boundary .*
44import scala .util .control .NonFatal
55import java .util .concurrent .ThreadFactory
6+ import scala .concurrent .duration .*
67
78enum ExitCode (val code : Int ):
89 case Success extends ExitCode (0 )
@@ -47,7 +48,11 @@ trait OxApp:
4748 }
4849
4950 // on shutdown, the above fork is cancelled, causing interruption
50- val interruptThread = new Thread (() => cancellableMainFork.cancel().discard)
51+ val interruptThread =
52+ new Thread (() =>
53+ if timeoutOption(settings.shutdownTimeout)(cancellableMainFork.cancel()).isEmpty then
54+ Console .err.println(s " Clean shutdown timed out after ${settings.shutdownTimeout}, exiting. " )
55+ )
5156 interruptThread.setName(" ox-interrupt-hook" )
5257 mountShutdownHook(interruptThread)
5358
@@ -90,15 +95,44 @@ object OxApp:
9095 * The thread factory that should be used to create threads in Ox scopes ([[supervised ]], [[unsupervised ]] etc.). Useful e.g. when
9196 * integrating with third-party libraries to propagate context across (virtual) thread boundaries. If left unspecified, the default
9297 * virtual thread factory is used.
98+ * @param shutdownTimeout
99+ * The maximum amount of time a clean shutdown might take. This might prevent deadlocks due to usage of `System.exit` in the user's
100+ * code. After the timeout passes, the application will forcibly exit.
93101 */
94102 case class Settings (
95103 interruptedExitCode : ExitCode ,
96104 handleInterruptedException : InterruptedException => Unit ,
97105 handleException : Throwable => Unit ,
98- threadFactory : Option [ThreadFactory ]
99- )
106+ threadFactory : Option [ThreadFactory ],
107+ shutdownTimeout : FiniteDuration
108+ ):
109+ // required for binary compatibility
110+ def this (
111+ interruptedExitCode : ExitCode ,
112+ handleInterruptedException : InterruptedException => Unit ,
113+ handleException : Throwable => Unit ,
114+ threadFactory : Option [ThreadFactory ]
115+ ) = this (interruptedExitCode, handleInterruptedException, handleException, threadFactory, 10 .seconds)
116+
117+ // required for binary compatibility
118+ def copy (
119+ interruptedExitCode : ExitCode ,
120+ handleInterruptedException : InterruptedException => Unit ,
121+ handleException : Throwable => Unit ,
122+ threadFactory : Option [ThreadFactory ]
123+ ): Settings = Settings (interruptedExitCode, handleInterruptedException, handleException, threadFactory, shutdownTimeout)
124+ end Settings
100125
101126 object Settings :
127+ // required for binary compatibility
128+ def apply (
129+ interruptedExitCode : ExitCode ,
130+ handleInterruptedException : InterruptedException => Unit ,
131+ handleException : Throwable => Unit ,
132+ threadFactory : Option [ThreadFactory ]
133+ ): Settings =
134+ Settings (interruptedExitCode, handleInterruptedException, handleException, threadFactory, 10 .seconds)
135+
102136 val DefaultLogException : Throwable => Unit = (t : Throwable ) =>
103137 val defaultHandler = Thread .getDefaultUncaughtExceptionHandler
104138 if defaultHandler != null then defaultHandler.uncaughtException(Thread .currentThread(), t) else t.printStackTrace()
@@ -111,7 +145,7 @@ object OxApp:
111145 case _ => logException(t2)
112146
113147 val Default : Settings =
114- Settings (ExitCode .Success , defaultHandleInterruptedException(DefaultLogException ), DefaultLogException , None )
148+ Settings (ExitCode .Success , defaultHandleInterruptedException(DefaultLogException ), DefaultLogException , None , 10 .seconds )
115149 end Settings
116150
117151 /** Simple variant of OxApp does not pass command line arguments and exits with exit code 0 if no exceptions were thrown. */
0 commit comments