diff --git a/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.api b/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.api index 9c383d2ef2e..a95abde8e87 100644 --- a/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.api +++ b/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.api @@ -1,9 +1,11 @@ public abstract interface class arrow/AutoCloseScope { public abstract fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public abstract fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; + public abstract fun onClose (Lkotlin/jvm/functions/Function1;)V } public final class arrow/AutoCloseScope$DefaultImpls { + public static fun autoClose (Larrow/AutoCloseScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; } @@ -16,6 +18,7 @@ public final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { public fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public final fun close (Ljava/lang/Throwable;)Ljava/lang/Void; public fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; + public fun onClose (Lkotlin/jvm/functions/Function1;)V } public final class arrow/ThrowIfFatalKt { diff --git a/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.klib.api b/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.klib.api index 57cfacdb144..9750d19eb80 100644 --- a/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.klib.api +++ b/arrow-libs/core/arrow-autoclose/api/arrow-autoclose.klib.api @@ -7,15 +7,16 @@ // Library unique name: abstract interface arrow/AutoCloseScope { // arrow/AutoCloseScope|null[0] - abstract fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] + abstract fun onClose(kotlin/Function1) // arrow/AutoCloseScope.onClose|onClose(kotlin.Function1){}[0] + open fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] open fun <#A1: kotlin/AutoCloseable> install(#A1): #A1 // arrow/AutoCloseScope.install|install(0:0){0§}[0] } final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { // arrow/DefaultAutoCloseScope|null[0] constructor () // arrow/DefaultAutoCloseScope.|(){}[0] - final fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/DefaultAutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] final fun close(kotlin/Throwable?): kotlin/Nothing? // arrow/DefaultAutoCloseScope.close|close(kotlin.Throwable?){}[0] + final fun onClose(kotlin/Function1) // arrow/DefaultAutoCloseScope.onClose|onClose(kotlin.Function1){}[0] } final fun (kotlin/Throwable).arrow/throwIfFatal(): kotlin/Throwable // arrow/throwIfFatal|throwIfFatal@kotlin.Throwable(){}[0] diff --git a/arrow-libs/core/arrow-autoclose/knit.code.include b/arrow-libs/core/arrow-autoclose/knit.code.include index 9aba59e0907..3a9e803c11c 100644 --- a/arrow-libs/core/arrow-autoclose/knit.code.include +++ b/arrow-libs/core/arrow-autoclose/knit.code.include @@ -1,5 +1,4 @@ // This file was automatically generated from ${file.name} by Knit tool. Do not edit. -@file:OptIn(ExperimentalStdlibApi::class) package ${knit.package}.${knit.name} import arrow.AutoCloseScope diff --git a/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt b/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt index d42f23cd54d..9f8f45fcecd 100644 --- a/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt +++ b/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt @@ -2,6 +2,7 @@ package arrow import arrow.atomic.Atomic import arrow.atomic.update +import arrow.atomic.value import kotlin.coroutines.cancellation.CancellationException /** @@ -63,50 +64,50 @@ import kotlin.coroutines.cancellation.CancellationException */ public inline fun autoCloseScope(block: AutoCloseScope.() -> A): A { val scope = DefaultAutoCloseScope() + var throwable: Throwable? = null return try { block(scope) - .also { scope.close(null) } - } catch (e: CancellationException) { - scope.close(e) ?: throw e } catch (e: Throwable) { - scope.close(e.throwIfFatal()) ?: throw e + throwable = e + throw e + } finally { + if (throwable !is CancellationException) throwable?.throwIfFatal() + scope.close(throwable) } } public interface AutoCloseScope { + public fun onClose(release: (Throwable?) -> Unit) + public fun autoClose( acquire: () -> A, release: (A, Throwable?) -> Unit - ): A + ): A = acquire().also { a -> onClose { release(a, it) } } - @ExperimentalStdlibApi public fun install(autoCloseable: A): A = - autoClose({ autoCloseable }) { a, _ -> a.close() } + autoCloseable.also { onClose { autoCloseable.close() } } } @PublishedApi internal class DefaultAutoCloseScope : AutoCloseScope { private val finalizers = Atomic(emptyList<(Throwable?) -> Unit>()) - override fun autoClose(acquire: () -> A, release: (A, Throwable?) -> Unit): A = - try { - acquire().also { a -> - finalizers.update { it + { e -> release(a, e) } } - } - } catch (e: Throwable) { - throw e - } + override fun onClose(release: (Throwable?) -> Unit) { + finalizers.update { it + release } + } fun close(error: Throwable?): Nothing? { - return finalizers.get().asReversed().fold(error) { acc, function -> - acc.add(runCatching { function.invoke(error) }.exceptionOrNull()) + return finalizers.value.asReversed().fold(error) { acc, finalizer -> + acc.add(runCatching { finalizer(error) }.exceptionOrNull()) }?.let { throw it } } - private fun Throwable?.add(other: Throwable?): Throwable? = - this?.apply { + private fun Throwable?.add(other: Throwable?): Throwable? { + if (other !is CancellationException) other?.throwIfFatal() + return this?.apply { other?.let { addSuppressed(it) } } ?: other + } } @PublishedApi diff --git a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt index 2dc8d63d192..7019f7cc8cc 100644 --- a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt +++ b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.test.runTest import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test -@OptIn(ExperimentalStdlibApi::class) class AutoCloseTest { @Test @@ -21,14 +20,14 @@ class AutoCloseTest { autoCloseScope { val r = autoClose({ res }) { r, e -> - promise.complete(e) + require(promise.complete(e)) r.shutdown() } - wasActive.complete(r.isActive()) + require(wasActive.complete(r.isActive())) } - promise.await() shouldBe null - wasActive.await() shouldBe true + promise.shouldHaveCompleted() shouldBe null + wasActive.shouldHaveCompleted() shouldBe true res.isActive() shouldBe false } @@ -42,16 +41,16 @@ class AutoCloseTest { shouldThrow { autoCloseScope { val r = autoClose({ res }) { r, e -> - promise.complete(e) + require(promise.complete(e)) r.shutdown() } - wasActive.complete(r.isActive()) + require(wasActive.complete(r.isActive())) throw error } } shouldBe error - promise.await() shouldBe error - wasActive.await() shouldBe true + promise.shouldHaveCompleted() shouldBe error + wasActive.shouldHaveCompleted() shouldBe true res.isActive() shouldBe false } @@ -67,20 +66,20 @@ class AutoCloseTest { val e = shouldThrow { autoCloseScope { val r = autoClose({ res }) { r, e -> - promise.complete(e) + require(promise.complete(e)) r.shutdown() throw error2 } autoClose({ Resource() }) { _, _ -> throw error3 } - wasActive.complete(r.isActive()) + require(wasActive.complete(r.isActive())) throw error } } e shouldBe error e.suppressedExceptions shouldBe listOf(error3, error2) - promise.await() shouldBe error - wasActive.await() shouldBe true + promise.shouldHaveCompleted() shouldBe error + wasActive.shouldHaveCompleted() shouldBe true res.isActive() shouldBe false } @@ -93,7 +92,7 @@ class AutoCloseTest { val e = shouldThrow { autoCloseScope { autoClose({ Resource() }) { r, e -> - promise.complete(e) + require(promise.complete(e)) r.shutdown() throw error2 } @@ -102,7 +101,7 @@ class AutoCloseTest { } e shouldBe error e.suppressedExceptions shouldBe listOf(error2) - promise.await() shouldBe error + promise.shouldHaveCompleted() shouldBe error } @Test @@ -112,10 +111,10 @@ class AutoCloseTest { autoCloseScope { val r = install(res) - wasActive.complete(r.isActive()) + require(wasActive.complete(r.isActive())) } - wasActive.await() shouldBe true + wasActive.shouldHaveCompleted() shouldBe true res.isActive() shouldBe false } @@ -127,12 +126,29 @@ class AutoCloseTest { shouldThrow { autoCloseScope { val r = install(res) - wasActive.complete(r.isActive()) + require(wasActive.complete(r.isActive())) throw CancellationException("BOOM!") } }.message shouldBe "BOOM!" - wasActive.await() shouldBe true + wasActive.shouldHaveCompleted() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun closeTheAutoScopeOnNonLocalReturn() = runTest { + val wasActive = CompletableDeferred() + val res = Resource() + + run { + autoCloseScope { + val r = install(res) + require(wasActive.complete(r.isActive())) + return@run + } + } + + wasActive.shouldHaveCompleted() shouldBe true res.isActive() shouldBe false } @@ -172,7 +188,6 @@ class AutoCloseTest { closed.cancel() } - @OptIn(ExperimentalStdlibApi::class) private class Resource : AutoCloseable { private val isActive = AtomicBoolean(true) @@ -188,4 +203,9 @@ class AutoCloseTest { shutdown() } } + + private suspend fun CompletableDeferred.shouldHaveCompleted(): T { + isCompleted shouldBe true + return await() + } } diff --git a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/arrow/AutoCloseJvmTest.kt b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/arrow/AutoCloseJvmTest.kt index a5f087aae40..f5236c67a52 100644 --- a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/arrow/AutoCloseJvmTest.kt +++ b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/arrow/AutoCloseJvmTest.kt @@ -3,30 +3,49 @@ package arrow import arrow.atomic.AtomicBoolean import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import kotlin.test.Test -@OptIn(ExperimentalStdlibApi::class) class AutoCloseJvmTest { @Test fun blowTheAutoScopeOnFatal() = runTest { - val wasActive = CompletableDeferred() + var wasActive = false val res = Resource() shouldThrow { autoCloseScope { val r = install(res) - wasActive.complete(r.isActive()) + wasActive = r.isActive() throw LinkageError("BOOM!") } }.message shouldBe "BOOM!" - wasActive.await() shouldBe true + wasActive shouldBe true res.isActive() shouldBe true } + @Test + fun blowTheAutoScopeOnFatalInClose() = runTest { + var wasActive = false + val res = Resource() + val res2 = Resource() + + shouldThrow { + autoCloseScope { + val r = install(res) + wasActive = r.isActive() + onClose { throw LinkageError("BOOM!") } + install(res2) + onClose { throw RuntimeException() } + } + }.message shouldBe "BOOM!" + + wasActive shouldBe true + res.isActive() shouldBe true + res2.isActive() shouldBe false + } + private class Resource : AutoCloseable { private val isActive = AtomicBoolean(true) diff --git a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-01.kt b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-01.kt index c31f45e3e20..6a95c607e92 100644 --- a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-01.kt +++ b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-01.kt @@ -1,5 +1,4 @@ // This file was automatically generated from AutoCloseScope.kt by Knit tool. Do not edit. -@file:OptIn(ExperimentalStdlibApi::class) package arrow.autocloseable.examples.exampleAutocloseable01 import arrow.AutoCloseScope diff --git a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-02.kt b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-02.kt index a944072c24e..fc4d78ef126 100644 --- a/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-02.kt +++ b/arrow-libs/core/arrow-autoclose/src/jvmTest/kotlin/examples/example-autocloseable-02.kt @@ -1,5 +1,4 @@ // This file was automatically generated from AutoCloseScope.kt by Knit tool. Do not edit. -@file:OptIn(ExperimentalStdlibApi::class) package arrow.autocloseable.examples.exampleAutocloseable02 import arrow.AutoCloseScope diff --git a/arrow-libs/core/arrow-autoclose/src/nonJvmMain/kotlin/arrow/throwIfFatal.kt b/arrow-libs/core/arrow-autoclose/src/nonJvmMain/kotlin/arrow/throwIfFatal.kt index 6a152b426b5..43a962579a3 100644 --- a/arrow-libs/core/arrow-autoclose/src/nonJvmMain/kotlin/arrow/throwIfFatal.kt +++ b/arrow-libs/core/arrow-autoclose/src/nonJvmMain/kotlin/arrow/throwIfFatal.kt @@ -1,4 +1,6 @@ package arrow +import kotlin.coroutines.cancellation.CancellationException + @PublishedApi -internal actual fun Throwable.throwIfFatal(): Throwable = this +internal actual fun Throwable.throwIfFatal(): Throwable = if (this is CancellationException) throw this else this diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index b98d51e7f13..669e1fea5a9 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -5,10 +5,11 @@ public final class arrow/fx/coroutines/AcquireStep { public final class arrow/fx/coroutines/BracketKt { public static final fun bracket (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun bracketCase (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun finalizeCase (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun getErrorOrNull (Larrow/fx/coroutines/ExitCase;)Ljava/lang/Throwable; public static final fun guarantee (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun guaranteeCase (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun onCancel (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun runReleaseAndRethrow (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class arrow/fx/coroutines/CountDownLatch { @@ -247,13 +248,18 @@ public final class arrow/fx/coroutines/ResourceKt { public abstract interface class arrow/fx/coroutines/ResourceScope : arrow/AutoCloseScope { public abstract fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun install (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun onClose (Lkotlin/jvm/functions/Function1;)V public abstract fun onRelease (Lkotlin/jvm/functions/Function2;)V public abstract fun release (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun releaseCase (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class arrow/fx/coroutines/ResourceScope$DefaultImpls { + public static fun autoClose (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun bind (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun install (Larrow/fx/coroutines/ResourceScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; + public static fun install (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun onClose (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function1;)V public static fun release (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun releaseCase (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.klib.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.klib.api index c4801b44f0b..09e4fdb83d8 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.klib.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.klib.api @@ -20,10 +20,11 @@ open annotation class arrow.fx.coroutines/ScopeDSL : kotlin/Annotation { // arro abstract interface arrow.fx.coroutines/ResourceScope : arrow/AutoCloseScope { // arrow.fx.coroutines/ResourceScope|null[0] abstract fun onRelease(kotlin.coroutines/SuspendFunction1) // arrow.fx.coroutines/ResourceScope.onRelease|onRelease(kotlin.coroutines.SuspendFunction1){}[0] - abstract suspend fun <#A1: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).bind(): #A1 // arrow.fx.coroutines/ResourceScope.bind|bind@kotlin.coroutines.SuspendFunction1(){0§}[0] - abstract suspend fun <#A1: kotlin/Any?> install(kotlin.coroutines/SuspendFunction1, kotlin.coroutines/SuspendFunction2<#A1, arrow.fx.coroutines/ExitCase, kotlin/Unit>): #A1 // arrow.fx.coroutines/ResourceScope.install|install(kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction2<0:0,arrow.fx.coroutines.ExitCase,kotlin.Unit>){0§}[0] + open fun onClose(kotlin/Function1) // arrow.fx.coroutines/ResourceScope.onClose|onClose(kotlin.Function1){}[0] + open suspend fun <#A1: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).bind(): #A1 // arrow.fx.coroutines/ResourceScope.bind|bind@kotlin.coroutines.SuspendFunction1(){0§}[0] open suspend fun <#A1: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).release(kotlin.coroutines/SuspendFunction1<#A1, kotlin/Unit>): #A1 // arrow.fx.coroutines/ResourceScope.release|release@kotlin.coroutines.SuspendFunction1(kotlin.coroutines.SuspendFunction1<0:0,kotlin.Unit>){0§}[0] open suspend fun <#A1: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).releaseCase(kotlin.coroutines/SuspendFunction2<#A1, arrow.fx.coroutines/ExitCase, kotlin/Unit>): #A1 // arrow.fx.coroutines/ResourceScope.releaseCase|releaseCase@kotlin.coroutines.SuspendFunction1(kotlin.coroutines.SuspendFunction2<0:0,arrow.fx.coroutines.ExitCase,kotlin.Unit>){0§}[0] + open suspend fun <#A1: kotlin/Any?> install(kotlin.coroutines/SuspendFunction1, kotlin.coroutines/SuspendFunction2<#A1, arrow.fx.coroutines/ExitCase, kotlin/Unit>): #A1 // arrow.fx.coroutines/ResourceScope.install|install(kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction2<0:0,arrow.fx.coroutines.ExitCase,kotlin.Unit>){0§}[0] } final class <#A: kotlin/Any?> arrow.fx.coroutines/ScopedRaiseAccumulate : arrow.core.raise/RaiseAccumulate<#A>, kotlinx.coroutines/CoroutineScope { // arrow.fx.coroutines/ScopedRaiseAccumulate|null[0] @@ -145,6 +146,9 @@ sealed class arrow.fx.coroutines/ExitCase { // arrow.fx.coroutines/ExitCase|null final object arrow.fx.coroutines/AcquireStep // arrow.fx.coroutines/AcquireStep|null[0] +final val arrow.fx.coroutines/errorOrNull // arrow.fx.coroutines/errorOrNull|@arrow.fx.coroutines.ExitCase{}errorOrNull[0] + final fun (arrow.fx.coroutines/ExitCase).(): kotlin/Throwable? // arrow.fx.coroutines/errorOrNull.|@arrow.fx.coroutines.ExitCase(){}[0] + final fun <#A: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.fx.coroutines/asFlow(): kotlinx.coroutines.flow/Flow<#A> // arrow.fx.coroutines/asFlow|asFlow@kotlin.coroutines.SuspendFunction1(){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).arrow.fx.coroutines/metered(kotlin.time/Duration): kotlinx.coroutines.flow/Flow<#A> // arrow.fx.coroutines/metered|metered@kotlinx.coroutines.flow.Flow<0:0>(kotlin.time.Duration){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).arrow.fx.coroutines/metered(kotlin/Long): kotlinx.coroutines.flow/Flow<#A> // arrow.fx.coroutines/metered|metered@kotlinx.coroutines.flow.Flow<0:0>(kotlin.Long){0§}[0] @@ -158,6 +162,7 @@ final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlinx.coroutines.flow/Flo final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).arrow.fx.coroutines/parMap(kotlin/Int = ..., crossinline kotlin.coroutines/SuspendFunction2): kotlinx.coroutines.flow/Flow<#B> // arrow.fx.coroutines/parMap|parMap@kotlinx.coroutines.flow.Flow<0:0>(kotlin.Int;kotlin.coroutines.SuspendFunction2){0§;1§}[0] final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).arrow.fx.coroutines/parMapNotNullUnordered(kotlin/Int = ..., crossinline kotlin.coroutines/SuspendFunction1<#A, #B?>): kotlinx.coroutines.flow/Flow<#B> // arrow.fx.coroutines/parMapNotNullUnordered|parMapNotNullUnordered@kotlinx.coroutines.flow.Flow<0:0>(kotlin.Int;kotlin.coroutines.SuspendFunction1<0:0,0:1?>){0§;1§}[0] final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).arrow.fx.coroutines/parMapUnordered(kotlin/Int = ..., crossinline kotlin.coroutines/SuspendFunction1<#A, #B>): kotlinx.coroutines.flow/Flow<#B> // arrow.fx.coroutines/parMapUnordered|parMapUnordered@kotlinx.coroutines.flow.Flow<0:0>(kotlin.Int;kotlin.coroutines.SuspendFunction1<0:0,0:1>){0§;1§}[0] +final inline fun <#A: kotlin/Any?> arrow.fx.coroutines/finalizeCase(kotlin/Function0<#A>, kotlin/Function1): #A // arrow.fx.coroutines/finalizeCase|finalizeCase(kotlin.Function0<0:0>;kotlin.Function1){0§}[0] final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (kotlin.collections/Iterable<#B>).arrow.fx.coroutines/parMapOrAccumulate(kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction2, #B, #C>): arrow.core/Either, kotlin.collections/List<#C>> // arrow.fx.coroutines/parMapOrAccumulate|parMapOrAccumulate@kotlin.collections.Iterable<0:1>(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction2,0:1,0:2>){0§;1§;2§}[0] final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (kotlin.collections/Iterable<#B>).arrow.fx.coroutines/parMapOrAccumulate(kotlin.coroutines/CoroutineContext = ..., kotlin/Function2<#A, #A, #A>, kotlin.coroutines/SuspendFunction2, #B, #C>): arrow.core/Either<#A, kotlin.collections/List<#C>> // arrow.fx.coroutines/parMapOrAccumulate|parMapOrAccumulate@kotlin.collections.Iterable<0:1>(kotlin.coroutines.CoroutineContext;kotlin.Function2<0:0,0:0,0:0>;kotlin.coroutines.SuspendFunction2,0:1,0:2>){0§;1§;2§}[0] final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (kotlin.collections/Iterable<#B>).arrow.fx.coroutines/parMapOrAccumulate(kotlin.coroutines/CoroutineContext = ..., kotlin/Int, kotlin.coroutines/SuspendFunction2, #B, #C>): arrow.core/Either, kotlin.collections/List<#C>> // arrow.fx.coroutines/parMapOrAccumulate|parMapOrAccumulate@kotlin.collections.Iterable<0:1>(kotlin.coroutines.CoroutineContext;kotlin.Int;kotlin.coroutines.SuspendFunction2,0:1,0:2>){0§;1§;2§}[0] @@ -166,11 +171,9 @@ final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.collections/Iterabl final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.collections/Iterable<#A>).arrow.fx.coroutines/parMap(kotlin.coroutines/CoroutineContext = ..., kotlin/Int, kotlin.coroutines/SuspendFunction2): kotlin.collections/List<#B> // arrow.fx.coroutines/parMap|parMap@kotlin.collections.Iterable<0:0>(kotlin.coroutines.CoroutineContext;kotlin.Int;kotlin.coroutines.SuspendFunction2){0§;1§}[0] final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.collections/Iterable<#A>).arrow.fx.coroutines/parMapNotNull(kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction2): kotlin.collections/List<#B> // arrow.fx.coroutines/parMapNotNull|parMapNotNull@kotlin.collections.Iterable<0:0>(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction2){0§;1§}[0] final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.collections/Iterable<#A>).arrow.fx.coroutines/parMapNotNull(kotlin.coroutines/CoroutineContext = ..., kotlin/Int, kotlin.coroutines/SuspendFunction2): kotlin.collections/List<#B> // arrow.fx.coroutines/parMapNotNull|parMapNotNull@kotlin.collections.Iterable<0:0>(kotlin.coroutines.CoroutineContext;kotlin.Int;kotlin.coroutines.SuspendFunction2){0§;1§}[0] -final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.fx.coroutines/use(kotlin.coroutines/SuspendFunction1<#A, #B>): #B // arrow.fx.coroutines/use|use@kotlin.coroutines.SuspendFunction1(kotlin.coroutines.SuspendFunction1<0:0,0:1>){0§;1§}[0] final suspend fun <#A: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.fx.coroutines/allocate(): kotlin/Pair<#A, kotlin.coroutines/SuspendFunction1> // arrow.fx.coroutines/allocate|allocate@kotlin.coroutines.SuspendFunction1(){0§}[0] final suspend fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).arrow.fx.coroutines.await/awaitAll(kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines.await/awaitAll|awaitAll@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.SuspendFunction1){0§}[0] final suspend fun <#A: kotlin/Any?> arrow.fx.coroutines.await/awaitAll(kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines.await/awaitAll|awaitAll(kotlin.coroutines.SuspendFunction1){0§}[0] -final suspend fun <#A: kotlin/Any?> arrow.fx.coroutines/resourceScope(kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines/resourceScope|resourceScope(kotlin.coroutines.SuspendFunction1){0§}[0] final suspend fun <#A: kotlin/AutoCloseable> (arrow.fx.coroutines/ResourceScope).arrow.fx.coroutines/autoCloseable(kotlinx.coroutines/CoroutineDispatcher = ..., kotlin.coroutines/SuspendFunction0<#A>): #A // arrow.fx.coroutines/autoCloseable|autoCloseable@arrow.fx.coroutines.ResourceScope(kotlinx.coroutines.CoroutineDispatcher;kotlin.coroutines.SuspendFunction0<0:0>){0§}[0] final suspend fun arrow.fx.coroutines/cancelAndCompose(kotlinx.coroutines/Deferred<*>, kotlinx.coroutines/Deferred<*>) // arrow.fx.coroutines/cancelAndCompose|cancelAndCompose(kotlinx.coroutines.Deferred<*>;kotlinx.coroutines.Deferred<*>){}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?, #D: kotlin/Any?, #E: kotlin/Any?, #F: kotlin/Any?, #G: kotlin/Any?, #H: kotlin/Any?, #I: kotlin/Any?, #J: kotlin/Any?, #K: kotlin/Any?> (arrow.core.raise/Raise<#A>).arrow.fx.coroutines/parZipOrAccumulate(crossinline kotlin/Function2<#A, #A, #A>, crossinline kotlin.coroutines/SuspendFunction1, #B>, crossinline kotlin.coroutines/SuspendFunction1, #C>, crossinline kotlin.coroutines/SuspendFunction1, #D>, crossinline kotlin.coroutines/SuspendFunction1, #E>, crossinline kotlin.coroutines/SuspendFunction1, #F>, crossinline kotlin.coroutines/SuspendFunction1, #G>, crossinline kotlin.coroutines/SuspendFunction1, #H>, crossinline kotlin.coroutines/SuspendFunction1, #I>, crossinline kotlin.coroutines/SuspendFunction1, #J>, crossinline kotlin.coroutines/SuspendFunction10): #K // arrow.fx.coroutines/parZipOrAccumulate|parZipOrAccumulate@arrow.core.raise.Raise<0:0>(kotlin.Function2<0:0,0:0,0:0>;kotlin.coroutines.SuspendFunction1,0:1>;kotlin.coroutines.SuspendFunction1,0:2>;kotlin.coroutines.SuspendFunction1,0:3>;kotlin.coroutines.SuspendFunction1,0:4>;kotlin.coroutines.SuspendFunction1,0:5>;kotlin.coroutines.SuspendFunction1,0:6>;kotlin.coroutines.SuspendFunction1,0:7>;kotlin.coroutines.SuspendFunction1,0:8>;kotlin.coroutines.SuspendFunction1,0:9>;kotlin.coroutines.SuspendFunction10){0§;1§;2§;3§;4§;5§;6§;7§;8§;9§;10§}[0] @@ -223,6 +226,7 @@ final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> arr final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> arrow.fx.coroutines/parZip(kotlin.coroutines/CoroutineContext = ..., crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction3): #C // arrow.fx.coroutines/parZip|parZip(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction3){0§;1§;2§}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> arrow.fx.coroutines/raceN(crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1): arrow.fx.coroutines/Race3<#A, #B, #C> // arrow.fx.coroutines/raceN|raceN(kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1){0§;1§;2§}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> arrow.fx.coroutines/raceN(kotlin.coroutines/CoroutineContext = ..., crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1): arrow.fx.coroutines/Race3<#A, #B, #C> // arrow.fx.coroutines/raceN|raceN(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1){0§;1§;2§}[0] +final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.fx.coroutines/use(kotlin.coroutines/SuspendFunction1<#A, #B>): #B // arrow.fx.coroutines/use|use@kotlin.coroutines.SuspendFunction1(kotlin.coroutines.SuspendFunction1<0:0,0:1>){0§;1§}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?> arrow.fx.coroutines/bracket(crossinline kotlin.coroutines/SuspendFunction0<#A>, kotlin.coroutines/SuspendFunction1<#A, #B>, crossinline kotlin.coroutines/SuspendFunction1<#A, kotlin/Unit>): #B // arrow.fx.coroutines/bracket|bracket(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction1<0:0,0:1>;kotlin.coroutines.SuspendFunction1<0:0,kotlin.Unit>){0§;1§}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?> arrow.fx.coroutines/bracketCase(crossinline kotlin.coroutines/SuspendFunction0<#A>, kotlin.coroutines/SuspendFunction1<#A, #B>, crossinline kotlin.coroutines/SuspendFunction2<#A, arrow.fx.coroutines/ExitCase, kotlin/Unit>): #B // arrow.fx.coroutines/bracketCase|bracketCase(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction1<0:0,0:1>;kotlin.coroutines.SuspendFunction2<0:0,arrow.fx.coroutines.ExitCase,kotlin.Unit>){0§;1§}[0] final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?> arrow.fx.coroutines/raceN(crossinline kotlin.coroutines/SuspendFunction1, crossinline kotlin.coroutines/SuspendFunction1): arrow.core/Either<#A, #B> // arrow.fx.coroutines/raceN|raceN(kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1){0§;1§}[0] @@ -230,4 +234,4 @@ final suspend inline fun <#A: kotlin/Any?, #B: kotlin/Any?> arrow.fx.coroutines/ final suspend inline fun <#A: kotlin/Any?> arrow.fx.coroutines/guarantee(kotlin.coroutines/SuspendFunction0<#A>, crossinline kotlin.coroutines/SuspendFunction0): #A // arrow.fx.coroutines/guarantee|guarantee(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction0){0§}[0] final suspend inline fun <#A: kotlin/Any?> arrow.fx.coroutines/guaranteeCase(kotlin.coroutines/SuspendFunction0<#A>, crossinline kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines/guaranteeCase|guaranteeCase(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction1){0§}[0] final suspend inline fun <#A: kotlin/Any?> arrow.fx.coroutines/onCancel(kotlin.coroutines/SuspendFunction0<#A>, crossinline kotlin.coroutines/SuspendFunction0): #A // arrow.fx.coroutines/onCancel|onCancel(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction0){0§}[0] -final suspend inline fun arrow.fx.coroutines/runReleaseAndRethrow(kotlin/Throwable, crossinline kotlin.coroutines/SuspendFunction0): kotlin/Nothing // arrow.fx.coroutines/runReleaseAndRethrow|runReleaseAndRethrow(kotlin.Throwable;kotlin.coroutines.SuspendFunction0){}[0] +final suspend inline fun <#A: kotlin/Any?> arrow.fx.coroutines/resourceScope(kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines/resourceScope|resourceScope(kotlin.coroutines.SuspendFunction1){0§}[0] diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt index 983dc48e558..0a98b60a085 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt @@ -20,6 +20,14 @@ public sealed class ExitCase { } } +@PublishedApi +internal val ExitCase.errorOrNull: Throwable? + get() = when (this) { + ExitCase.Completed -> null + is ExitCase.Cancelled -> exception + is ExitCase.Failure -> failure + } + /** * Registers an [onCancel] handler after [fa]. * [onCancel] is guaranteed to be called in case of cancellation, otherwise it's ignored. @@ -59,17 +67,7 @@ public suspend inline fun onCancel( public suspend inline fun guarantee( fa: suspend () -> A, crossinline finalizer: suspend () -> Unit -): A { - val res = try { - fa.invoke() - } catch (e: CancellationException) { - runReleaseAndRethrow(e) { finalizer() } - } catch (t: Throwable) { - runReleaseAndRethrow(t.nonFatalOrThrow()) { finalizer() } - } - withContext(NonCancellable) { finalizer() } - return res -} +): A = guaranteeCase(fa) { finalizer() } /** * Guarantees execution of a given [finalizer] after [fa] regardless of success, error or cancellation, allowing @@ -89,16 +87,19 @@ public suspend inline fun guarantee( public suspend inline fun guaranteeCase( fa: suspend () -> A, crossinline finalizer: suspend (ExitCase) -> Unit -): A { - val res = try { - fa() - } catch (e: CancellationException) { - runReleaseAndRethrow(e) { finalizer(ExitCase.Cancelled(e)) } - } catch (t: Throwable) { - runReleaseAndRethrow(t.nonFatalOrThrow()) { finalizer(ExitCase.Failure(t)) } +): A = finalizeCase({ fa() }) { ex -> + try { + withContext(NonCancellable) { + finalizer(ex) + } + } catch (e: Throwable) { + e.nonFatalOrThrow() + when (ex) { + ExitCase.Completed -> throw e + is ExitCase.Failure -> ex.failure.addSuppressed(e) + is ExitCase.Cancelled -> ex.exception.addSuppressed(e) + } } - withContext(NonCancellable) { finalizer(ExitCase.Completed) } - return res } /** @@ -146,22 +147,7 @@ public suspend inline fun bracket( crossinline acquire: suspend () -> A, use: suspend (A) -> B, crossinline release: suspend (A) -> Unit -): B { - val acquired = withContext(NonCancellable) { - acquire() - } - - val res = try { - use(acquired) - } catch (e: CancellationException) { - runReleaseAndRethrow(e) { release(acquired) } - } catch (t: Throwable) { - runReleaseAndRethrow(t.nonFatalOrThrow()) { release(acquired) } - } - - withContext(NonCancellable) { release(acquired) } - return res -} +): B = bracketCase(acquire, use) { acquired, _ -> release(acquired) } /** * A way to safely acquire a resource and release in the face of errors and cancellation. @@ -231,31 +217,20 @@ public suspend inline fun bracketCase( use: suspend (A) -> B, crossinline release: suspend (A, ExitCase) -> Unit ): B { - val acquired = withContext(NonCancellable) { - acquire() - } - - val res = try { - use(acquired) - } catch (e: CancellationException) { - runReleaseAndRethrow(e) { release(acquired, ExitCase.Cancelled(e)) } - } catch (t: Throwable) { - runReleaseAndRethrow(t.nonFatalOrThrow()) { release(acquired, ExitCase.Failure(t.nonFatalOrThrow())) } - } - - withContext(NonCancellable) { release(acquired, ExitCase.Completed) } - - return res + val acquired = withContext(NonCancellable) { acquire() } + return guaranteeCase({ use(acquired) }) { release(acquired, it) } } @PublishedApi -internal suspend inline fun runReleaseAndRethrow(original: Throwable, crossinline f: suspend () -> Unit): Nothing { - try { - withContext(NonCancellable) { - f() - } +internal inline fun finalizeCase(block: () -> R, finalizer: (ExitCase) -> Unit): R { + var exitCase: ExitCase = ExitCase.Completed + return try { + block() } catch (e: Throwable) { - original.addSuppressed(e.nonFatalOrThrow()) + exitCase = ExitCase.ExitCase(e) + throw e + } finally { + if (exitCase is ExitCase.Failure) exitCase.failure.nonFatalOrThrow() + finalizer(exitCase) } - throw original } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 93ef675adb1..a220f0b8c07 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -1,19 +1,18 @@ package arrow.fx.coroutines import arrow.AutoCloseScope -import arrow.atomic.update import arrow.atomic.Atomic +import arrow.atomic.update import arrow.atomic.value -import arrow.core.identity +import arrow.core.nonFatalOrThrow import arrow.core.prependTo -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -import kotlin.jvm.JvmInline +import kotlin.coroutines.cancellation.CancellationException @DslMarker public annotation class ScopeDSL @@ -293,7 +292,8 @@ public interface ResourceScope : AutoCloseScope { * All [release] functions [install]ed into the [Resource] lambda will be installed in this [ResourceScope] while respecting the FIFO order. */ @ResourceDSL - public suspend fun Resource.bind(): A + public suspend fun Resource.bind(): A = this() + /** * Install [A] into the [ResourceScope]. * Its [release] function will be called with the appropriate [ExitCase] if this [ResourceScope] finishes. @@ -303,21 +303,21 @@ public interface ResourceScope : AutoCloseScope { public suspend fun install( acquire: suspend AcquireStep.() -> A, release: suspend (A, ExitCase) -> Unit, - ): A + ): A = withContext(NonCancellable) { + acquire(AcquireStep).also { a -> onRelease { release(a, it) } } + } /** Composes a [release] action to a [Resource] value before binding. */ @ResourceDSL - public suspend infix fun Resource.release(release: suspend (A) -> Unit): A { - val a = bind() - return install({ a }) { aa, _ -> release(aa) } - } + public suspend infix fun Resource.release(release: suspend (A) -> Unit): A = + bind().also { a -> onRelease { release(a) } } /** Composes a [releaseCase] action to a [Resource] value before binding. */ @ResourceDSL - public suspend infix fun Resource.releaseCase(release: suspend (A, ExitCase) -> Unit): A { - val a = bind() - return install({ a }, release) - } + public suspend infix fun Resource.releaseCase(release: suspend (A, ExitCase) -> Unit): A = + bind().also { a -> onRelease { release(a, it) } } + + override fun onClose(release: (Throwable?) -> Unit): Unit = onRelease { release(it.errorOrNull) } public infix fun onRelease(release: suspend (ExitCase) -> Unit) } @@ -353,24 +353,15 @@ public fun resource(block: suspend ResourceScope.() -> A): Resource = blo * */ @ScopeDSL -public suspend fun resourceScope(action: suspend ResourceScope.() -> A): A { - val scope = ResourceScopeImpl() - val a: A = try { - action(scope) - } catch (e: Throwable) { - val ex = if (e is CancellationException) ExitCase.Cancelled(e) else ExitCase.Failure(e) - val ee = withContext(NonCancellable) { - scope.cancelAll(ex, e) ?: e - } - throw ee - } - withContext(NonCancellable) { - scope.cancelAll(ExitCase.Completed)?.let { throw it } - } - return a +@OptIn(DelicateCoroutinesApi::class) +@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") +public suspend inline fun resourceScope(action: suspend ResourceScope.() -> A): A { + val (scope, cancelAll) = resource { this }.allocate() + return finalizeCase({ scope.action() }) { cancelAll(it) } } -public suspend infix fun Resource.use(f: suspend (A) -> B): B = +@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") +public suspend inline infix fun Resource.use(f: suspend (A) -> B): B = resourceScope { f(bind()) } /** @@ -401,7 +392,7 @@ public fun resource( } /** - * Runs [Resource.use] and emits [A] of the resource + * Runs [Resource].[use] and emits [A] of the resource * * ```kotlin * import arrow.fx.coroutines.* @@ -468,72 +459,29 @@ public fun Resource.asFlow(): Flow = * This API is useful for building inter-op APIs between [Resource] and non-suspending code, such as Java libraries. */ @DelicateCoroutinesApi -public suspend fun Resource.allocate(): Pair Unit> { - val effect = ResourceScopeImpl() - val allocated: A = invoke(effect) - val release: suspend (ExitCase) -> Unit = { e -> - val suppressed: Throwable? = effect.cancelAll(e) - val original: Throwable? = when(e) { - ExitCase.Completed -> null - is ExitCase.Cancelled -> e.exception - is ExitCase.Failure -> e.failure - } - original?.apply { - suppressed?.let(::addSuppressed) - }?.let { throw it } - } - return Pair(allocated, release) +public suspend fun Resource.allocate(): Pair Unit> = with(ResourceScopeImpl()) { + bind() to this::cancelAll } -@JvmInline -private value class ResourceScopeImpl( - private val finalizers: Atomic Unit>> = Atomic(emptyList()), -) : ResourceScope { - override suspend fun Resource.bind(): A = invoke(this@ResourceScopeImpl) - +internal class ResourceScopeImpl : ResourceScope { + private val finalizers: Atomic Unit>> = Atomic(emptyList()) override fun onRelease(release: suspend (ExitCase) -> Unit) { finalizers.update(release::prependTo) } - override suspend fun install(acquire: suspend AcquireStep.() -> A, release: suspend (A, ExitCase) -> Unit): A = - bracketCase({ - val a = acquire(AcquireStep) - val finalizer: suspend (ExitCase) -> Unit = { ex: ExitCase -> release(a, ex) } - finalizers.update(finalizer::prependTo) - a - }, ::identity, { a, ex -> - // Only if ExitCase.Failure, or ExitCase.Cancelled during acquire we cancel - // Otherwise we've saved the finalizer, and it will be called from somewhere else. - if (ex != ExitCase.Completed) { - val e = cancelAll(ex) - val e2 = kotlin.runCatching { release(a, ex) }.exceptionOrNull() - e?.apply { - e2?.let(::addSuppressed) - }?.let { throw it } + suspend fun cancelAll(exitCase: ExitCase) { + withContext(NonCancellable) { + finalizers.value.fold(exitCase.errorOrNull) { acc, finalizer -> + acc.add(runCatching { finalizer(exitCase) }.exceptionOrNull()) } - }) - - override fun autoClose(acquire: () -> A, release: (A, Throwable?) -> Unit): A = - acquire().also { a -> - val finalizer: suspend (ExitCase) -> Unit = { exitCase -> - val errorOrNull = when (exitCase) { - ExitCase.Completed -> null - is ExitCase.Cancelled -> exitCase.exception - is ExitCase.Failure -> exitCase.failure - } - release(a, errorOrNull) - } - finalizers.update(finalizer::prependTo) - } + }?.let { throw it } + } - suspend fun cancelAll( - exitCase: ExitCase, - first: Throwable? = null, - ): Throwable? = finalizers.value.fold(first) { acc, finalizer -> - val other = kotlin.runCatching { finalizer(exitCase) }.exceptionOrNull() - other?.let { - acc?.apply { addSuppressed(other) } ?: other - } ?: acc + private fun Throwable?.add(other: Throwable?): Throwable? { + if (other !is CancellationException) other?.nonFatalOrThrow() + return this?.apply { + other?.let { addSuppressed(it) } + } ?: other } } @@ -561,7 +509,7 @@ internal expect val IODispatcher: CoroutineDispatcher public suspend fun ResourceScope.autoCloseable( closingDispatcher: CoroutineDispatcher = IODispatcher, autoCloseable: suspend () -> A, -): A = install({ autoCloseable() } ) { s: A, _: ExitCase -> withContext(closingDispatcher) { s.close() } } +): A = install({ autoCloseable() } ) { s: A, _ -> withContext(closingDispatcher) { s.close() } } public fun autoCloseable( closingDispatcher: CoroutineDispatcher = IODispatcher, diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/BracketCaseTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/BracketCaseTest.kt index b5a881959a1..e11d2eda82a 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/BracketCaseTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/BracketCaseTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.time.ExperimentalTime @@ -54,7 +55,7 @@ class BracketCaseTest { bracketCase( acquire = { throw e }, use = { 5 }, - release = { _, _ -> Unit } + release = { _, _ -> } ) } should leftException(e) } @@ -67,7 +68,7 @@ class BracketCaseTest { bracketCase( acquire = { e.suspend() }, use = { 5 }, - release = { _, _ -> Unit } + release = { _, _ -> } ) } should leftException(e) } @@ -122,6 +123,44 @@ class BracketCaseTest { } } + @Test + fun bracketCaseMustRunReleaseTaskOnUseEarlyReturn() = runTest { + checkAll(10, Arb.int()) { i -> + val promise = CompletableDeferred() + + run { + bracketCase( + acquire = { i }, + use = { return@run it }, + release = { _, ex -> + require(promise.complete(ex)) { "Release should only be called once, called again with $ex" } + } + ) + } shouldBe i + + promise.await() shouldBe ExitCase.Completed + } + } + + @Test + fun bracketCaseMustRunReleaseTaskOnUseSuspendedEarlyReturn() = runTest { + checkAll(10, Arb.int()) { i -> + val promise = CompletableDeferred() + + run { + bracketCase( + acquire = { i }, + use = { return@run it.suspend() }, + release = { _, ex -> + require(promise.complete(ex)) { "Release should only be called once, called again with $ex" } + } + ) + } shouldBe i + + promise.await() shouldBe ExitCase.Completed + } + } + @Test fun bracketCaseMustRunReleaseTaskOnUseSuspendedError() = runTest { checkAll(10, Arb.int(), Arb.throwable()) { x, e -> @@ -206,58 +245,66 @@ class BracketCaseTest { } } - operator fun Throwable.plus(other: Throwable): Throwable = - apply { addSuppressed(other) } + private infix fun Throwable.shouldHaveSuppressed(exception: Throwable) = + suppressedExceptions shouldBe listOf(exception) @Test fun bracketCaseMustComposeImmediateUseAndImmediateReleaseError() = runTest { - checkAll(10, Arb.int(), Arb.throwable(), Arb.throwable()) { n, e, e2 -> + checkAll(10, Arb.int(), Arb.throwableConstructor(), Arb.throwable()) { n, eConstructor, e2 -> + val e = eConstructor() Either.catch { bracketCase( acquire = { n }, use = { throw e }, release = { _, _ -> throw e2 } ) - } shouldBe Either.Left(e + e2) + } shouldBe Either.Left(e) + e shouldHaveSuppressed e2 } } @Test fun bracketCaseMustComposeSuspendUseAndImmediateReleaseError() = runTest { - checkAll(10, Arb.int(), Arb.throwable(), Arb.throwable()) { n, e, e2 -> + checkAll(10, Arb.int(), Arb.throwableConstructor(), Arb.throwable()) { n, eConstructor, e2 -> + val e = eConstructor() Either.catch { bracketCase( acquire = { n }, use = { e.suspend() }, release = { _, _ -> throw e2 } ) - } shouldBe Either.Left(e + e2) + } shouldBe Either.Left(e) + e shouldHaveSuppressed e2 } } @Test fun bracketCaseMustComposeImmediateUseAndSuspendReleaseError() = runTest { - checkAll(10, Arb.int(), Arb.throwable(), Arb.throwable()) { n, e, e2 -> + checkAll(10, Arb.int(), Arb.throwableConstructor(), Arb.throwable()) { n, eConstructor, e2 -> + val e = eConstructor() Either.catch { bracketCase( acquire = { n }, use = { throw e }, release = { _, _ -> e2.suspend() } ) - } shouldBe Either.Left(e + e2) + } shouldBe Either.Left(e) + e shouldHaveSuppressed e2 } } @Test fun bracketCaseMustComposeSuspendUseAndSuspendReleaseError() = runTest { - checkAll(10, Arb.int(), Arb.throwable(), Arb.throwable()) { n, e, e2 -> + checkAll(10, Arb.int(), Arb.throwableConstructor(), Arb.throwable()) { n, eConstructor, e2 -> + val e = eConstructor() Either.catch { bracketCase( acquire = { n }, use = { e.suspend() }, release = { _, _ -> e2.suspend() } ) - } shouldBe Either.Left(e + e2) + } shouldBe Either.Left(e) + e shouldHaveSuppressed e2 } } @@ -318,7 +365,7 @@ class BracketCaseTest { val f = async { bracketCase( - acquire = { Unit }, + acquire = { }, use = { Unit.suspend() }, release = { _, exitCase -> require(exit.complete(exitCase)) { "Release should only be called once, called again with $exitCase" } @@ -357,7 +404,7 @@ class BracketCaseTest { // Wait until acquire started latch.await() - async { fiber.cancel() } + launch { fiber.cancel() } mVar.receive() shouldBe x mVar.receive() shouldBe y @@ -380,7 +427,7 @@ class BracketCaseTest { } latch.await() - async { fiber.cancel() } + launch { fiber.cancel() } mVar.receive() shouldBe x // If release was cancelled this hangs since the buffer is empty diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/Generators.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/Generators.kt index 98996374d23..61a2565028b 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/Generators.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/Generators.kt @@ -5,6 +5,7 @@ import io.kotest.assertions.fail import io.kotest.matchers.Matcher import io.kotest.matchers.MatcherResult import io.kotest.matchers.equalityMatcher +import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.choice import io.kotest.property.arbitrary.choose @@ -40,6 +41,9 @@ fun Arb.Companion.functionAToB(arb: Arb): Arb<(A) -> B> = fun Arb.Companion.throwable(): Arb = Arb.of(listOf(IndexOutOfBoundsException(), NoSuchElementException(), IllegalArgumentException())) +fun Arb.Companion.throwableConstructor(): Arb<() -> Throwable> = + Arb.of(listOf({ IndexOutOfBoundsException() }, { NoSuchElementException() }, { IllegalArgumentException() })) + fun Arb.Companion.either(arbE: Arb, arbA: Arb): Arb> { val arbLeft = arbE.map { Either.Left(it) } val arbRight = arbA.map { Either.Right(it) } @@ -81,17 +85,20 @@ fun leftException(e: Throwable): Matcher> = { "Expected exception of type ${e::class} but found ${value.value::class}" }, { "Should not be exception of type ${e::class}" } ) + value.value.message != e.message -> MatcherResult( false, { "Expected exception with message ${e.message} but found ${value.value.message}" }, { "Should not be exception with message ${e.message}" } ) + else -> MatcherResult( true, { "Expected exception of type ${e::class} and found ${value.value::class}" }, { "Expected exception of type ${e::class} and found ${value.value::class}" } ) } + is Either.Right -> MatcherResult( false, { "Expected Either.Left with exception of type ${e::class} and found Right with ${value.value}" }, @@ -122,6 +129,7 @@ fun either(e: Either): Matcher> = { "Expected exception of type ${e::class} and found ${value.value::class}" } ) } + is Either.Right -> equalityMatcher(e).test(value) } } @@ -147,3 +155,8 @@ inline fun assertThrowable(executable: () -> A): Throwable { return if (a is Throwable) a else fail("Expected an exception but found: $a") } + +suspend fun CompletableDeferred.shouldHaveCompleted(): T { + isCompleted shouldBe true + return await() +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceAutoCloseTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceAutoCloseTest.kt index 9494bc00416..7a6f38bbfe6 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceAutoCloseTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceAutoCloseTest.kt @@ -3,9 +3,14 @@ package arrow.fx.coroutines import arrow.atomic.AtomicBoolean import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf import io.kotest.property.Arb import io.kotest.property.checkAll +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield import kotlin.test.Test class ResourceAutoCloseTest { @@ -40,4 +45,36 @@ class ResourceAutoCloseTest { t.didClose.get() shouldBe true } } + + + @Test + fun autoClosableIsNonCancellable() = runTest { + val t = AutoCloseableTest() + val exit = CompletableDeferred() + val waitingToBeCancelled = CompletableDeferred() + val cancelled = CompletableDeferred() + + val job = launch { + resourceScope { + onRelease { require(exit.complete(it)) } + autoCloseable { + waitingToBeCancelled.complete(Unit) + cancelled.await() + t + } + yield() + } + } + + waitingToBeCancelled.await() + job.cancel("BOOM!") + cancelled.complete(Unit) + job.join() + + t.didClose.get() shouldBe true + exit.shouldHaveCompleted() + .shouldBeTypeOf() + .exception + .message shouldBe "BOOM!" + } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index a890450c8e4..5ac980fbd46 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -27,12 +27,15 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlinx.coroutines.channels.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield class ResourceTest { @@ -70,16 +73,15 @@ class ResourceTest { resourceScope { install({ n }) { _, ex -> require(p.complete(ex)) } } - p.await() shouldBe ExitCase.Completed + p.shouldHaveCompleted() shouldBe ExitCase.Completed } } @Test fun errorFinishesWithError() = runTest { checkAll(10, Arb.throwable()) { e -> - val p = CompletableDeferred() suspend fun ResourceScope.failingScope(): Nothing = - install({ throw e }, { _, ex -> require(p.complete(ex)) }) + install({ throw e }, { _, _ -> fail("should not get here") }) Either.catch { resourceScope { failingScope() } @@ -133,7 +135,7 @@ class ResourceTest { raise("error") } } shouldBe "error".left() - exit.await().shouldBeTypeOf() + exit.shouldHaveCompleted().shouldBeTypeOf() } private val depth = 10 @@ -156,7 +158,35 @@ class ResourceTest { } }.message shouldBe "BOOM!" - exit.await() + exit.shouldHaveCompleted() + .shouldBeTypeOf() + .exception + .message shouldBe "BOOM!" + } + + @Test + fun installIsNonCancellable() = runTest { + val exit = CompletableDeferred() + val waitingToBeCancelled = CompletableDeferred() + val cancelled = CompletableDeferred() + + val job = launch { + resourceScope { + install({ + waitingToBeCancelled.complete(Unit) + cancelled.await() + }) { _, ex -> + require(exit.complete(ex)) + } + yield() + } + } + waitingToBeCancelled.await() + job.cancel("BOOM!") + cancelled.complete(Unit) + job.join() + + exit.shouldHaveCompleted() .shouldBeTypeOf() .exception .message shouldBe "BOOM!" @@ -483,11 +513,11 @@ class ResourceTest { } } shouldBe throwable - val (aa, exA) = releasedA.await() + val (aa, exA) = releasedA.shouldHaveCompleted() aa shouldBe a exA.shouldBeTypeOf() - val (bb, exB) = releasedB.await() + val (bb, exB) = releasedB.shouldHaveCompleted() bb shouldBe b exB.shouldBeTypeOf() } @@ -510,11 +540,11 @@ class ResourceTest { } } - val (aa, exA) = releasedA.await() + val (aa, exA) = releasedA.shouldHaveCompleted() aa shouldBe a exA.shouldBeTypeOf() - val (bb, exB) = releasedB.await() + val (bb, exB) = releasedB.shouldHaveCompleted() bb shouldBe b exB.shouldBeTypeOf() } @@ -528,7 +558,7 @@ class ResourceTest { r.asFlow().map { it + 1 }.toList() shouldBe listOf(n + 1) - released.await() shouldBe ExitCase.Completed + released.shouldHaveCompleted() shouldBe ExitCase.Completed } } @@ -542,7 +572,7 @@ class ResourceTest { r.asFlow().collect { throw throwable } } shouldBe throwable - released.await().shouldBeTypeOf().failure shouldBe throwable + released.shouldHaveCompleted().shouldBeTypeOf().failure shouldBe throwable } } @@ -556,7 +586,7 @@ class ResourceTest { r.asFlow().collect { throw CancellationException("") } } - released.await().shouldBeTypeOf() + released.shouldHaveCompleted().shouldBeTypeOf() } } @@ -565,12 +595,12 @@ class ResourceTest { fun allocateWorks() = runTest { checkAll(10, Arb.int()) { seed -> val released = CompletableDeferred() - val (allocated, release) = resource({ seed }) { _, exitCase -> released.complete(exitCase) } + val (allocated, release) = resource({ seed }) { _, exitCase -> require(released.complete(exitCase)) } .allocate() allocated shouldBe seed release(ExitCase.Completed) - released.await() shouldBe ExitCase.Completed + released.shouldHaveCompleted() shouldBe ExitCase.Completed } } @@ -585,7 +615,7 @@ class ResourceTest { val released = CompletableDeferred() val (allocated, release) = resource({ seed }) { _, exitCase -> - released.complete(exitCase) + require(released.complete(exitCase)) throw suppressed }.allocate() @@ -600,7 +630,7 @@ class ResourceTest { exception shouldBe original exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe suppressed - released.await().shouldBeTypeOf() + released.shouldHaveCompleted().shouldBeTypeOf() } } @@ -615,7 +645,7 @@ class ResourceTest { val released = CompletableDeferred() val (allocated, release) = resource({ seed }) { _, exitCase -> - released.complete(exitCase) + require(released.complete(exitCase)) throw suppressed }.allocate() @@ -630,11 +660,45 @@ class ResourceTest { exception shouldBe cancellation exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe suppressed - released.await().shouldBeTypeOf() + released.shouldHaveCompleted().shouldBeTypeOf() + } + } + + @OptIn(DelicateCoroutinesApi::class) + @Test + fun allocatedSuppressedExceptions() = runTest { + checkAll( + Arb.int(), + Arb.string().map(::RuntimeException), + Arb.string().map(::IllegalStateException), + Arb.string().map(::IllegalStateException), + ) { seed, original, suppressed1, suppressed2 -> + val released = CompletableDeferred() + val (allocate, release) = + resource { + onRelease { exitCase -> + require(released.complete(exitCase)) + throw suppressed1 + } + onClose { throw suppressed2 } + seed + }.allocate() + + val exception = shouldThrow { + try { + allocate shouldBe seed + throw original + } catch (e: Throwable) { + release(ExitCase(e)) + } + } + + exception shouldBe original + exception.suppressedExceptions shouldBe listOf(suppressed2, suppressed1) + released.shouldHaveCompleted() shouldBe ExitCase(original) } } - @OptIn(ExperimentalStdlibApi::class) private class Res : AutoCloseable { private val isActive = AtomicBoolean(true) @@ -686,4 +750,62 @@ class ResourceTest { closed.receive() shouldBe res1 closed.cancel() } + + @Test + fun addsSuppressedErrors() = runTest { + val exitCase = CompletableDeferred() + val wasActive = CompletableDeferred() + val error = RuntimeException("BOOM!") + val error2 = RuntimeException("BOOM 2!") + val error3 = RuntimeException("BOOM 3!") + val res = Res() + + val e = shouldThrow { + resourceScope { + val r = install({ res }) { r, e -> + require(exitCase.complete(e)) + r.shutdown() + throw error2 + } + onClose { throw error3 } + require(wasActive.complete(r.isActive())) + throw error + } + } + + e shouldBe error + e.suppressedExceptions shouldBe listOf(error3, error2) + exitCase.shouldHaveCompleted() shouldBe ExitCase(error) + wasActive.shouldHaveCompleted() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun addsSuppressedErrorsFromReleasers() = runTest { + val exitCase = CompletableDeferred() + val wasActive = CompletableDeferred() + val error = RuntimeException("BOOM!") + val error2 = RuntimeException("BOOM 2!") + val error3 = RuntimeException("BOOM 3!") + val res = Res() + + val e = shouldThrow { + resourceScope { + val r = install({ res }) { r, e -> + require(exitCase.complete(e)) + r.shutdown() + throw error2 + } + onClose { throw error3 } + require(wasActive.complete(r.isActive())) + onRelease { throw error } + } + } + + e shouldBe error + e.suppressedExceptions shouldBe listOf(error3, error2) + exitCase.shouldHaveCompleted() shouldBe ExitCase.Completed + wasActive.shouldHaveCompleted() shouldBe true + res.isActive() shouldBe false + } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/BracketCaseJvmTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/BracketCaseJvmTest.kt new file mode 100644 index 00000000000..4eba484065c --- /dev/null +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/BracketCaseJvmTest.kt @@ -0,0 +1,28 @@ +package arrow.fx.coroutines + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import kotlin.test.DefaultAsserter.fail +import kotlin.test.Test + +class BracketCaseJvmTest { + @Test + fun blowBracketOnFatal() = runTest { + val error = shouldThrow { + bracket({ }, { throw LinkageError("BOOM!") }) { fail("Should never come here") } + } + error.message shouldBe "BOOM!" + error.suppressedExceptions.shouldBeEmpty() + } + + @Test + fun blowBracketOnFatalInRelease() = runTest { + val error = shouldThrow { + bracket({ }, { throw RuntimeException() }) { throw LinkageError("BOOM!") } + } + error.message shouldBe "BOOM!" + error.suppressedExceptions.shouldBeEmpty() + } +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/ResourceTestJvm.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/ResourceTestJvm.kt index e16df010762..fb807443e28 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/ResourceTestJvm.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/ResourceTestJvm.kt @@ -1,6 +1,7 @@ package arrow.fx.coroutines import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.shouldBe import java.util.concurrent.atomic.AtomicBoolean import java.lang.AutoCloseable @@ -74,11 +75,29 @@ class ResourceTestJvm { @Test fun blowTheScopeOnFatal() = runTest { - shouldThrow { + val error = shouldThrow { resourceScope { install({ }) { _, _ -> fail("Should never come here") } throw LinkageError("BOOM!") } - }.message shouldBe "BOOM!" + } + error.message shouldBe "BOOM!" + error.suppressedExceptions.shouldBeEmpty() + } + + @Test + fun blowTheScopeOnFatalInRelease() = runTest { + var isLastClosed = false + val error = shouldThrow { + resourceScope { + install({ }) { _, _ -> fail("Should never come here") } + onRelease { throw LinkageError("BOOM!") } + onClose { isLastClosed = true } + onClose { throw RuntimeException() } + } + } + error.message shouldBe "BOOM!" + error.suppressedExceptions.shouldBeEmpty() + isLastClosed shouldBe true } }