Graceful shutdown
When building applications that require graceful shutdown it typically requires us to write a lot of platform-specific code. This library aims to solve that problem by leveraging Kotlin MPP using KotlinX Coroutines, and Structured Concurrency.
- Graceful Shutdown with Structured Concurrency by Simon Vergauwen
Graceful shutdowns are implemented in the suspendapp
library, with further integration with other frameworks available in their own libraries. For historical reasons, the suspendapp
library follows its own versioning scheme. Please check Maven Central for the latest release information.
Simple example
When you see App Started! Waiting until asked to shutdown.
try pressing
Ctrl+C to signal interruption (SIGINT
) to the process.
You can also use ps -ax
to find the PID and call kill PID
to send a
SIGTERM
event to the process.
import arrow.continuations.SuspendApp
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
fun main() = SuspendApp {
try {
println("App Started! Waiting until asked to shutdown.")
while (true) {
delay(2_500)
println("Ping")
}
} catch (e: CancellationException) {
println("Cleaning up App... will take 10 seconds...")
withContext(NonCancellable) { delay(10_000) }
println("Done cleaning up. Will release app to exit")
}
}
Since our CoroutineScope
is cancelled we need to run our delay
in NonCancellable
.
SuspendApp Arrow's Resource
Resource
allows for modeling resources within the suspend
world,
and properly takes into account structured concurrency and cancellation.
This means that when a CoroutineScope
gets cancelled, then any suspend finalizer will back pressure Job#join
.
And thus when you call cancelAndJoin
on a CoroutineScope
it will properly await the finalizers
to have finished
running.
With SuspendApp this means that if someone sends a terminal signal such as SIGINT
or SIGTERM
to the application
then it will run all the suspend finalizers before closing the application.
fun main() = SuspendApp {
resourceScope {
install({ println("Creating some resource") }) { _, exitCase ->
println("ExitCase: $exitCase")
println("Shutting down will take 10 seconds")
delay(10_000)
println("Shutdown finished")
}
println("Application running with acquired resources.")
awaitCancellation()
}
}
In the example above we have a Resource
that during acquisition will print Creating some resource
,
when the Resource
needs to be closed, release, we print the ExitCase
with which the Resource
was closed, and
then we wait for 10 seconds. The Resource
already takes care of calling release
on a NonCancellable
context.
We consume the Resource
until our application is cancelled by calling awaitCancellation
from KotlinX Coroutines.
That gives us the following output, if you press Ctrl+C in the terminal.
Creating some resource
Application running with acquired resources.
^CExitCase: Cancelled(exception=kotlinx.coroutines.JobCancellationException: LazyStandaloneCoroutine was cancelled; job=LazyStandaloneCoroutine{Cancelling}@f7470010)
Shutting down will take 10 seconds
Shutdown finished
You can find this example in the repository, currently setup for NodeJS and native targets.
Running on different platforms
For more details on Kotlin Multiplatform configuration consult the official documentation.
Just ./gradlew build
the project, and launch the created binaries as shown in the sections belows.
- JVM
- MacOsX64 & MacosArm64
- NodeJS
- Windows (MingwX64)
- Linux
SuspendApp currently does not support any mobile or browser targets because it does not make sense to have such application behavior on such platforms. If you have a use-case for this please open a ticket!
Node.js
Make sure you configure your NodeJS app to be executable.
js(IR) {
nodejs {
binaries.executable()
}
}
You can run your NodeJS app with the following node
command,
and if you press Ctrl+C within the first 2500ms you will see the following output.
node build/js/packages/YourAppName/kotlin/YourAppName.js
App Started! Waiting until asked to shutdown.
^CCleaning up App... will take 10 seconds...
Done cleaning up. Will release app to exit
Native
Make sure you configure your Native app(s) to be executable.
linuxX64 {
binaries.executable()
}
mingwX64 {
binaries.executable()
}
macosArm64 {
binaries.executable()
}
macosX64 {
binaries.executable()
}
You can run your Native app with the following command, and if you press Ctrl+C within the first 2500ms you will see the following output.
./gradlew build
build/bin/native/releaseExecutable/YourAppName.kexe
App Started! Waiting until asked to shutdown.
^CCleaning up App... will take 10 seconds...
Done cleaning up. Will release app to exit