From 1a066b9fd665f09a7f10d61c2af8e338c21d4d0a Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 29 Nov 2025 20:27:50 +0000 Subject: [PATCH 1/2] Implement Saga in terms of Resource --- .../arrow-resilience/build.gradle.kts | 3 +- .../kotlin/arrow/resilience/Saga.kt | 69 ++++++------------- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/arrow-libs/resilience/arrow-resilience/build.gradle.kts b/arrow-libs/resilience/arrow-resilience/build.gradle.kts index 915eb86b010..f9e495e745f 100644 --- a/arrow-libs/resilience/arrow-resilience/build.gradle.kts +++ b/arrow-libs/resilience/arrow-resilience/build.gradle.kts @@ -6,8 +6,7 @@ kotlin { sourceSets { commonMain { dependencies { - api(projects.arrowCore) - implementation(libs.coroutines.core) + api(projects.arrowFxCoroutines) } } commonTest { diff --git a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt index ae8190c4fcc..7b9f13e661e 100644 --- a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt +++ b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt @@ -3,9 +3,10 @@ package arrow.resilience import arrow.atomic.Atomic import arrow.atomic.update import arrow.core.nonFatalOrThrow -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.withContext -import kotlin.coroutines.cancellation.CancellationException +import arrow.core.prependTo +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.ResourceScope +import arrow.fx.coroutines.resourceScope /** @@ -98,14 +99,8 @@ public fun saga( * fails then all compensating actions are guaranteed to run. When a compensating action failed it * will be ignored, and the other compensating actions will continue to be run. */ -public suspend fun Saga.transact(): A { - val builder = SagaBuilder() - return guaranteeCase({ invoke(builder) }) { res -> - when (res) { - null -> builder.totalCompensation() - else -> Unit - } - } +public suspend fun Saga.transact(): A = resourceScope { + invoke(SagaResourceScope(this)) } /** DSL Marker for the SagaEffect DSL */ @@ -114,10 +109,19 @@ public suspend fun Saga.transact(): A { /** * Marker object to protect [SagaScope.saga] from calling [SagaScope.bind] in its `action` step. */ -@SagaDSLMarker public object SagaActionStep // Internal implementation of the `saga { }` builder. +private class SagaResourceScope(private val scope: ResourceScope) : SagaScope { + override suspend fun saga( + action: suspend SagaActionStep.() -> A, + compensation: suspend (A) -> Unit + ): A = action(SagaActionStep).also { a -> + scope.onRelease { if (it !is ExitCase.Completed) compensation(a) } + } +} + +@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN) @PublishedApi internal class SagaBuilder( private val stack: Atomic Unit>> = Atomic(emptyList()) @@ -127,19 +131,11 @@ internal class SagaBuilder( override suspend fun saga( action: suspend SagaActionStep.() -> A, compensation: suspend (A) -> Unit - ): A = - guaranteeCase({ action(SagaActionStep) }) { res -> - // This action failed, so we have no compensate to push on the stack - // the compensation stack will run in the `transact` stage, this is just the builder - when (res) { - null -> Unit - else -> stack.update( - function = { listOf(suspend { compensation(res) }) + it }, - transform = { _, new -> new } - ) - } - } + ): A = action(SagaActionStep).also { res -> + stack.update(suspend { compensation(res) }::prependTo) + } + @Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN) @PublishedApi internal suspend fun totalCompensation() { stack @@ -156,28 +152,3 @@ internal class SagaBuilder( ?.let { throw it } } } - -private suspend fun guaranteeCase( - fa: suspend () -> A, - finalizer: suspend (value: A?) -> Unit -): A { - val res = - try { - fa() - } catch (e: CancellationException) { - runReleaseAndRethrow(e) { finalizer(null) } - } catch (t: Throwable) { - runReleaseAndRethrow(t) { finalizer(null) } - } - withContext(NonCancellable) { finalizer(res) } - return res -} - -private suspend fun runReleaseAndRethrow(original: Throwable, f: suspend () -> Unit): Nothing { - try { - withContext(NonCancellable) { f() } - } catch (e: Throwable) { - original.addSuppressed(e.nonFatalOrThrow()) - } - throw original -} From 61069541b3c7077c32456f23d6fb7e415fa8ebad Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 29 Nov 2025 20:30:28 +0000 Subject: [PATCH 2/2] Update API files --- .../arrow-resilience/api/android/arrow-resilience.api | 2 +- .../resilience/arrow-resilience/api/jvm/arrow-resilience.api | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow-libs/resilience/arrow-resilience/api/android/arrow-resilience.api b/arrow-libs/resilience/arrow-resilience/api/android/arrow-resilience.api index a46f4c489b3..033e1229277 100644 --- a/arrow-libs/resilience/arrow-resilience/api/android/arrow-resilience.api +++ b/arrow-libs/resilience/arrow-resilience/api/android/arrow-resilience.api @@ -107,7 +107,7 @@ public final class arrow/resilience/SagaBuilder : arrow/resilience/SagaScope { public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun saga (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun totalCompensation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final synthetic fun totalCompensation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface annotation class arrow/resilience/SagaDSLMarker : java/lang/annotation/Annotation { diff --git a/arrow-libs/resilience/arrow-resilience/api/jvm/arrow-resilience.api b/arrow-libs/resilience/arrow-resilience/api/jvm/arrow-resilience.api index a46f4c489b3..033e1229277 100644 --- a/arrow-libs/resilience/arrow-resilience/api/jvm/arrow-resilience.api +++ b/arrow-libs/resilience/arrow-resilience/api/jvm/arrow-resilience.api @@ -107,7 +107,7 @@ public final class arrow/resilience/SagaBuilder : arrow/resilience/SagaScope { public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun saga (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun totalCompensation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final synthetic fun totalCompensation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface annotation class arrow/resilience/SagaDSLMarker : java/lang/annotation/Annotation {