From 21db530b2e891b898342fa0dd9199ff2ced4e5bc Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 1 Nov 2024 14:40:07 +0000 Subject: [PATCH] Make `install` non-cancellable and add tests for it Add suppression for REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE --- .../api/arrow-fx-coroutines.klib.api | 2 +- .../kotlin/arrow/fx/coroutines/Resource.kt | 14 ++++--- .../fx/coroutines/ResourceAutoCloseTest.kt | 37 +++++++++++++++++++ .../arrow/fx/coroutines/ResourceTest.kt | 31 ++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) 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 482910c5e17..69074c3bafb 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 @@ -177,6 +177,7 @@ final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlin.collections/Iterabl final suspend fun <#A: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.fx.coroutines/allocated(): kotlin/Pair<#A, kotlin.coroutines/SuspendFunction1> // arrow.fx.coroutines/allocated|allocated@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/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] 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(kotlin.coroutines/CoroutineContext, 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.coroutines.CoroutineContext;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] @@ -237,5 +238,4 @@ final suspend inline fun <#A: kotlin/Any?> arrow.fx.coroutines/guarantee(kotlin. 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 <#A: kotlin/Any?> arrow.fx.coroutines/resourceScope(kotlin.coroutines/SuspendFunction1): #A // arrow.fx.coroutines/resourceScope|resourceScope(kotlin.coroutines.SuspendFunction1){0§}[0] -final suspend inline 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 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] 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 8a586d9aa64..19d2f092f02 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 @@ -301,7 +301,9 @@ public interface ResourceScope : AutoCloseScope { public suspend fun install( acquire: suspend AcquireStep.() -> A, release: suspend (A, ExitCase) -> Unit, - ): A = acquire(AcquireStep).also { a -> onRelease { release(a, it) } } + ): A = withContext(NonCancellable) { + acquire(AcquireStep).also { a -> onRelease { release(a, it) } } + } /** Composes a [release] action to a [Resource] value before binding. */ @ResourceDSL @@ -349,6 +351,7 @@ public fun resource(block: suspend ResourceScope.() -> A): Resource = blo * */ @ScopeDSL +@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") public suspend inline fun resourceScope(action: suspend ResourceScope.() -> A): A { val scope = ResourceScopeImpl() var finished = false @@ -365,6 +368,7 @@ public suspend inline fun resourceScope(action: suspend ResourceScope.() -> } } +@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") public suspend inline infix fun Resource.use(f: suspend (A) -> B): B = resourceScope { f(bind()) } @@ -396,7 +400,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.* @@ -510,14 +514,14 @@ internal expect val IODispatcher: CoroutineDispatcher * */ @ResourceDSL -public suspend inline fun ResourceScope.autoCloseable( +public suspend fun ResourceScope.autoCloseable( closingDispatcher: CoroutineDispatcher = IODispatcher, autoCloseable: suspend () -> A, -): A = autoCloseable().also { s -> onRelease { withContext(closingDispatcher) { s.close() } } } +): A = install({ autoCloseable() } ) { s: A, _ -> withContext(closingDispatcher) { s.close() } } public fun autoCloseable( closingDispatcher: CoroutineDispatcher = IODispatcher, autoCloseable: suspend () -> A, ): Resource = resource { - autoCloseable(closingDispatcher) { autoCloseable() } + autoCloseable(closingDispatcher, autoCloseable) } 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..8d3d5d8d104 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() + lateinit var exit: ExitCase + val waitingToBeCancelled = CompletableDeferred() + val cancelled = CompletableDeferred() + + val job = launch { + resourceScope { + onRelease { exit = 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 + .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 0e00137e84b..82719bd3a1b 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 { @@ -162,6 +165,34 @@ class ResourceTest { .message shouldBe "BOOM!" } + @Test + fun installIsNonCancellable() = runTest { + lateinit var exit: ExitCase + val waitingToBeCancelled = CompletableDeferred() + val cancelled = CompletableDeferred() + + val job = launch { + resourceScope { + install({ + waitingToBeCancelled.complete(Unit) + cancelled.await() + }) { _, ex -> + exit = ex + } + yield() + } + } + waitingToBeCancelled.await() + job.cancel("BOOM!") + cancelled.complete(Unit) + job.join() + + exit + .shouldBeTypeOf() + .exception + .message shouldBe "BOOM!" + } + @Test fun parZipSuccess() = runTestUsingDefaultDispatcher { suspend fun ResourceScope.closeable(): CheckableAutoClose =