Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions core/src/main/scala/ox/OxApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ox
import scala.util.boundary.*
import scala.util.control.NonFatal
import java.util.concurrent.ThreadFactory
import scala.concurrent.duration.*

enum ExitCode(val code: Int):
case Success extends ExitCode(0)
Expand Down Expand Up @@ -47,7 +48,11 @@ trait OxApp:
}

// on shutdown, the above fork is cancelled, causing interruption
val interruptThread = new Thread(() => cancellableMainFork.cancel().discard)
val interruptThread =
new Thread(() =>
if timeoutOption(settings.shutdownTimeout)(cancellableMainFork.cancel()).isEmpty then
Console.err.println(s"Clean shutdown timed out after ${settings.shutdownTimeout}, exiting.")
)
interruptThread.setName("ox-interrupt-hook")
mountShutdownHook(interruptThread)

Expand Down Expand Up @@ -90,15 +95,44 @@ object OxApp:
* The thread factory that should be used to create threads in Ox scopes ([[supervised]], [[unsupervised]] etc.). Useful e.g. when
* integrating with third-party libraries to propagate context across (virtual) thread boundaries. If left unspecified, the default
* virtual thread factory is used.
* @param shutdownTimeout
* The maximum amount of time a clean shutdown might take. This might prevent deadlocks due to usage of `System.exit` in the user's
* code. After the timeout passes, the application will forcibly exit.
*/
case class Settings(
interruptedExitCode: ExitCode,
handleInterruptedException: InterruptedException => Unit,
handleException: Throwable => Unit,
threadFactory: Option[ThreadFactory]
)
threadFactory: Option[ThreadFactory],
shutdownTimeout: FiniteDuration
):
// required for binary compatibility
def this(
interruptedExitCode: ExitCode,
handleInterruptedException: InterruptedException => Unit,
handleException: Throwable => Unit,
threadFactory: Option[ThreadFactory]
) = this(interruptedExitCode, handleInterruptedException, handleException, threadFactory, 10.seconds)

// required for binary compatibility
def copy(
interruptedExitCode: ExitCode,
handleInterruptedException: InterruptedException => Unit,
handleException: Throwable => Unit,
threadFactory: Option[ThreadFactory]
): Settings = Settings(interruptedExitCode, handleInterruptedException, handleException, threadFactory, shutdownTimeout)
end Settings

object Settings:
// required for binary compatibility
def apply(
interruptedExitCode: ExitCode,
handleInterruptedException: InterruptedException => Unit,
handleException: Throwable => Unit,
threadFactory: Option[ThreadFactory]
): Settings =
Settings(interruptedExitCode, handleInterruptedException, handleException, threadFactory, 10.seconds)

val DefaultLogException: Throwable => Unit = (t: Throwable) =>
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler
if defaultHandler != null then defaultHandler.uncaughtException(Thread.currentThread(), t) else t.printStackTrace()
Expand All @@ -111,7 +145,7 @@ object OxApp:
case _ => logException(t2)

val Default: Settings =
Settings(ExitCode.Success, defaultHandleInterruptedException(DefaultLogException), DefaultLogException, None)
Settings(ExitCode.Success, defaultHandleInterruptedException(DefaultLogException), DefaultLogException, None, 10.seconds)
end Settings

/** Simple variant of OxApp does not pass command line arguments and exits with exit code 0 if no exceptions were thrown. */
Expand Down
3 changes: 3 additions & 0 deletions doc/utils/oxapp.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ All `ox.OxApp` instances can be configured by overriding the `def settings: Sett
* `threadFactory`: the thread factory that is used to create threads in Ox scopes ([[supervised]], [[unsupervised]]
etc.). Useful e.g. when integrating with third-party libraries to propagate context across (virtual) thread
boundaries.
* `shutdownTimeout`: the maximum amount of time a clean shutdown might take. By default 10 seconds. This might
prevent deadlocks due to usage of `System.exit` in the user's code. After the timeout passes, the application
will forcibly exit.

Settings can be overridden:

Expand Down