Skip to content

Commit

Permalink
Merge pull request #120 from arrow-kt/exit-process
Browse files Browse the repository at this point in the history
Split platform code into Process interface, automatically call exit for 0 & -1
  • Loading branch information
nomisRev committed May 8, 2024
2 parents 62a3f8d + 187fbee commit 2914f41
Show file tree
Hide file tree
Showing 24 changed files with 382 additions and 241 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/generate-alpha-tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand All @@ -42,7 +42,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.TOKEN_GITHUB_ACTION }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/generate-tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand All @@ -60,7 +60,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.TOKEN_GITHUB_ACTION }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
timeout-minutes: 150
runs-on: macos-latest
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.TOKEN_GITHUB_ACTION }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand Down Expand Up @@ -44,7 +44,7 @@ jobs:
timeout-minutes: 60

steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- uses: actions/checkout@v4
with:
fetch-depth: 0

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,5 @@ _site/
.sass-cache/
.jekyll-cache/
.jekyll-metadata

.kotlin
26 changes: 15 additions & 11 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ dependencies {

## Rationale

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.
When writing software we need to deal with the lifecycle of the application such as termination signals, and sending
correct exit codes. This is important, so we correctly interact with the OS our application runs on.
This requires a lot of platform-specific code, SuspendApp solves that problem by leveraging Kotlin MPP using KotlinX Coroutines, and Structured Concurrency.
See [Simple Example below](#simple-example).

Currently supported targets:

Expand Down Expand Up @@ -118,16 +119,18 @@ shutdown.
More information on this can be found in this blog by [Phil Pearl](https://philpearl.github.io/post/k8s_ingress/),
and on [learnk8s.io](https://learnk8s.io/graceful-shutdown).

The module `suspendapp-ktor` provides a `server` constructor that lifts the Ktor `ApplicationEngine` in to a `Resource`,
The module `suspendapp-ktor` provides a `server` constructor that lifts the Ktor `ApplicationEngine` in to a `Resource`,
representing the _Engine_ running an `Application`(i.e `Netty`) while supporting auto-reload.
Check the official Ktor documentation to learn more about [watchPaths](https://ktor.io/docs/auto-reload.html).

When our `release` function of our `ApplicationEngine` is called, there is a `wait` period before the beginning of the stop
process (defaulted to `30.seconds`), this gives Kubernetes enough time to do all its network management before we shut down.
Two more parameters are available: `grace` which set the number of seconds during which already inflight requests are
allowed to continue before the shutdown process begins, and `timeout` which set the number of seconds after which the
When our `release` function of our `ApplicationEngine` is called, there is a `wait` period before the beginning of the
stop
process (defaulted to `30.seconds`), this gives Kubernetes enough time to do all its network management before we shut
down.
Two more parameters are available: `grace` which set the number of seconds during which already inflight requests are
allowed to continue before the shutdown process begins, and `timeout` which set the number of seconds after which the
server will be forceably shutdown. In the case that `ktor` server is set in
[development mode](https://ktor.io/docs/development-mode.html), the `wait` period is ignored.
[development mode](https://ktor.io/docs/development-mode.html), the `wait` period is ignored.

Given this `Resource` definition of a Ktor server, with support for gracefully shutting down for K8S we can define
a `SuspendApp`.
Expand Down Expand Up @@ -218,7 +221,8 @@ 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.

```text
node build/js/packages/YourAppName/kotlin/YourAppName.js
./gradlew compileProductionExecutableKotlinJs
node example/build/compileSync/js/main/productionExecutable/kotlin/suspendapp-example.js
App Started! Waiting until asked to shutdown.
^CCleaning up App... will take 10 seconds...
Expand Down Expand Up @@ -248,7 +252,7 @@ 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.

```text
./gradlew build
./gradlew linkReleaseExecutableMacosArm64
build/bin/native/releaseExecutable/YourAppName.kexe
App Started! Waiting until asked to shutdown.
Expand Down
32 changes: 25 additions & 7 deletions api/suspendapp.api
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
public final class arrow/continuations/SuspendAppKt {
public static final fun SuspendApp-8Mi8wO0 (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
public static synthetic fun SuspendApp-8Mi8wO0$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public final class arrow/continuations/Enviroment_jvmKt {
public static final fun process ()Larrow/continuations/Process;
}

public final class arrow/continuations/JvmProcess : arrow/continuations/Process {
public static final field INSTANCE Larrow/continuations/JvmProcess;
public fun close ()V
public fun exit (I)Ljava/lang/Void;
public synthetic fun exit (I)V
public fun onShutdown (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
public fun onSigInt (Lkotlin/jvm/functions/Function2;)V
public fun onSigTerm (Lkotlin/jvm/functions/Function2;)V
public fun runScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
}

public final class arrow/continuations/unsafe/Unsafe {
public static final field INSTANCE Larrow/continuations/unsafe/Unsafe;
public final fun onShutdown (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0;
public final fun runCoroutineScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
public abstract interface class arrow/continuations/Process : java/lang/AutoCloseable {
public abstract fun close ()V
public abstract fun exit (I)V
public abstract fun onShutdown (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
public abstract fun onSigInt (Lkotlin/jvm/functions/Function2;)V
public abstract fun onSigTerm (Lkotlin/jvm/functions/Function2;)V
public abstract fun runScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
}

public final class arrow/continuations/SuspendAppKt {
public static final fun SuspendApp-1Y68eR8 (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;JLarrow/continuations/Process;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun SuspendApp-1Y68eR8$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;JLarrow/continuations/Process;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}

13 changes: 8 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

@Suppress("DSL_SCOPE_VIOLATION")
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.dokka)
Expand Down Expand Up @@ -32,7 +31,8 @@ kotlin {
js(IR) {
nodejs()
}


linuxArm64()
linuxX64()
mingwX64()
macosArm64()
Expand All @@ -51,13 +51,15 @@ kotlin {
val linuxX64Main by getting
val macosArm64Main by getting
val macosX64Main by getting

val linuxArm64Main by getting

create("nativeMain") {
dependsOn(commonMain)
mingwX64Main.dependsOn(this)
linuxX64Main.dependsOn(this)
macosArm64Main.dependsOn(this)
macosX64Main.dependsOn(this)
mingwX64Main.dependsOn(this)
linuxArm64Main.dependsOn(this)
}
}
}
Expand All @@ -73,6 +75,7 @@ tasks {
matchingRegex.set(".*\\.unsafe.*")
suppress.set(true)
}
externalDocumentationLink("https://kotlinlang.org/api/kotlinx.coroutines/")
sourceLink {
localDirectory.set(file("src/commonMain/kotlin"))
remoteUrl.set(uri("https://github.com/arrow-kt/suspendapp/tree/main/src/commonMain/kotlin").toURL())
Expand All @@ -81,7 +84,7 @@ tasks {
}
}
}

withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = "1.8"
}
Expand Down
51 changes: 24 additions & 27 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.RELEASE

plugins {
kotlin("multiplatform")
}

repositories {
mavenCentral()
mavenCentral()
}

kotlin {
// TODO fix setup for Main-Class
// jvm()
js(IR) {
nodejs {
binaries.executable()
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
mainRun {
mainClass.set("io.arrow.suspendapp.example.MaintKt")
}
withJava()
}

linuxX64 {
binaries.executable()
}
mingwX64 {
binaries.executable()
}
macosArm64 {
js(IR) {
nodejs()
binaries.executable()
}
macosX64 {
binaries.executable()

listOf(
linuxX64(),
mingwX64(),
macosArm64(),
macosX64()
).forEach { target ->
target.binaries.executable(listOf(RELEASE)) {
entryPoint = "io.arrow.suspendapp.example.main"
}
}

sourceSets {
val commonMain by getting {
dependencies {
implementation(project.rootProject)
implementation("io.arrow-kt:arrow-fx-coroutines:1.2.0")
}
}
// val jvmMain by getting

val jvmMain by getting
val jsMain by getting
val mingwX64Main by getting
val linuxX64Main by getting
val macosArm64Main by getting
val macosX64Main by getting

create("nativeMain") {
dependsOn(commonMain)
mingwX64Main.dependsOn(this)
linuxX64Main.dependsOn(this)
macosArm64Main.dependsOn(this)
macosX64Main.dependsOn(this)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package io.arrow.suspendapp.example

import arrow.continuations.SuspendApp
import arrow.fx.coroutines.resource
import kotlinx.coroutines.awaitCancellation
Expand Down
11 changes: 0 additions & 11 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Package definitions
projects.group=io.arrow-kt

# Pomfile definitions
pom.name=suspendapp
pom.description=Reason about resource-safety in the same way you reason about Structured Concurrency with SuspendApp!
pom.url=https://github.com/arrow-kt/suspendapp/
Expand All @@ -13,17 +11,8 @@ pom.smc.url=https://github.com/arrow-kt/arrow/
pom.smc.connection=scm:git:git://github.com/arrow-kt/arrow.git
pom.smc.developerConnection=scm:git:ssh://[email protected]/arrow-kt/arrow.git

# Gradle options
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true

# To disable publishing of sha-512 checksums for maven-metadata.xml files
systemProp.org.gradle.internal.publish.checksums.insecure=true

# Kotlin configuration
kotlin.incremental=true
kotlin.code.style=official
kotlin.mpp.enableCompatibilityMetadataVariant=true
kotlin.native.enableDependencyPropagation=false
kotlin.js.generate.executable.default=false
kotlin.native.binary.memoryModel=experimental
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.9.10"
coroutines = "1.7.2"
kotlin = "1.9.24"
coroutines = "1.8.0"
dokka = "1.9.20"
arrowGradleConfig = "0.12.0-rc.6"
kotlinBinaryCompatibilityValidator = "0.14.0"
Expand Down
35 changes: 35 additions & 0 deletions src/commonMain/kotlin/arrow/continuations/Process.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package arrow.continuations

import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope

/** KMP constructor for [Process]. */
expect fun process(): Process

/**
* [Process] offers a common API to work with our application's process, installing signal handlers,
* shutdown hooks, running scopes in our process (runBlocking), and exiting the process.
*/
@OptIn(ExperimentalStdlibApi::class)
interface Process : AutoCloseable {
fun onSigTerm(block: suspend (code: Int) -> Unit): Unit

fun onSigInt(block: suspend (code: Int) -> Unit): Unit

fun onShutdown(block: suspend () -> Unit): suspend () -> Unit

/**
* On JVM, and Native this will use kotlinx.coroutines.runBlocking, On NodeJS we need an infinite
* heartbeat to keep main alive. The heartbeat is fast enough that it isn't silently discarded, as
* longer ticks are, but slow enough that we don't interrupt often.
* https://stackoverflow.com/questions/23622051/how-to-forcibly-keep-a-node-js-process-from-terminating
*/
fun runScope(
context: CoroutineContext,
block: suspend CoroutineScope.() -> Unit,
)

fun exit(code: Int): Unit

override fun close(): Unit
}
Loading

0 comments on commit 2914f41

Please sign in to comment.