Skip to content

Commit

Permalink
Tracing POC (#2946)
Browse files Browse the repository at this point in the history
Co-authored-by: Alejandro Serrano <trupill@gmail.com>
Co-authored-by: franciscodr <francisco.d@47deg.com>
Co-authored-by: Youssef Shoaib <canonballt@gmail.com>
  • Loading branch information
4 people committed Mar 16, 2023
1 parent 4a869af commit 0a86ec2
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 44 deletions.
28 changes: 22 additions & 6 deletions arrow-libs/core/arrow-core/api/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -3231,13 +3231,8 @@ public final class arrow/core/continuations/result {
public final fun invoke-gIAlu-s (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public class arrow/core/raise/CancellationExceptionNoTrace : java/util/concurrent/CancellationException {
public fun <init> ()V
public fun fillInStackTrace ()Ljava/lang/Throwable;
}

public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise {
public fun <init> ()V
public fun <init> (Z)V
public fun attempt (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun bind (Larrow/core/Either;)Ljava/lang/Object;
public fun bind (Larrow/core/Validated;)Ljava/lang/Object;
Expand All @@ -3249,10 +3244,14 @@ public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise {
public final fun complete ()Z
public fun invoke (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun isTraced ()Z
public fun raise (Ljava/lang/Object;)Ljava/lang/Void;
public fun shift (Ljava/lang/Object;)Ljava/lang/Object;
}

public abstract interface annotation class arrow/core/raise/ExperimentalTraceApi : java/lang/annotation/Annotation {
}

public final class arrow/core/raise/IorRaise : arrow/core/raise/Raise {
public fun <init> (Lkotlin/jvm/functions/Function2;Ljava/util/concurrent/atomic/AtomicReference;Larrow/core/raise/Raise;)V
public fun attempt (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down Expand Up @@ -3456,6 +3455,7 @@ public final class arrow/core/raise/RaiseKt {
public static final fun toResult (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun toValidated (Lkotlin/jvm/functions/Function1;)Larrow/core/Validated;
public static final fun toValidated (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun traced (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function9;)Ljava/lang/Object;
public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function8;)Ljava/lang/Object;
public static final fun zipOrAccumulate (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function7;)Ljava/lang/Object;
Expand Down Expand Up @@ -3514,6 +3514,22 @@ public final class arrow/core/raise/ResultRaise : arrow/core/raise/Raise {
public final synthetic fun unbox-impl ()Larrow/core/raise/Raise;
}

public final class arrow/core/raise/Trace {
public static final synthetic fun box-impl (Ljava/util/concurrent/CancellationException;)Larrow/core/raise/Trace;
public static fun constructor-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/concurrent/CancellationException;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Ljava/util/concurrent/CancellationException;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Ljava/util/concurrent/CancellationException;Ljava/util/concurrent/CancellationException;)Z
public fun hashCode ()I
public static fun hashCode-impl (Ljava/util/concurrent/CancellationException;)I
public static final fun printStackTrace-impl (Ljava/util/concurrent/CancellationException;)V
public static final fun stackTraceToString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String;
public static final fun suppressedExceptions-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/List;
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Ljava/util/concurrent/CancellationException;
}

public abstract interface class arrow/typeclasses/Monoid : arrow/typeclasses/Semigroup {
public static final field Companion Larrow/typeclasses/Monoid$Companion;
public static fun Boolean ()Larrow/typeclasses/Monoid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import arrow.core.Option
import arrow.core.Some
import arrow.core.getOrElse
import arrow.core.identity
import arrow.core.orElse
import arrow.typeclasses.Semigroup
import arrow.typeclasses.SemigroupDeprecation
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmInline
Expand All @@ -27,23 +23,16 @@ import kotlin.jvm.JvmName
public inline fun <E, A> either(@BuilderInference block: Raise<E>.() -> A): Either<E, A> =
fold({ block.invoke(this) }, { Either.Left(it) }, { Either.Right(it) })

public inline fun <A> nullable(block: NullableRaise.() -> A): A? {
contract { callsInPlace(block, EXACTLY_ONCE) }
return fold({ block(NullableRaise(this)) }, { null }, ::identity)
}
public inline fun <A> nullable(block: NullableRaise.() -> A): A? =
fold({ block(NullableRaise(this)) }, { null }, ::identity)

public inline fun <A> result(block: ResultRaise.() -> A): Result<A> {
contract { callsInPlace(block, EXACTLY_ONCE) }
return fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success)
}
public inline fun <A> result(block: ResultRaise.() -> A): Result<A> =
fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success)

public inline fun <A> option(block: OptionRaise.() -> A): Option<A> {
contract { callsInPlace(block, EXACTLY_ONCE) }
return fold({ block(OptionRaise(this)) }, ::identity, ::Some)
}
public inline fun <A> option(block: OptionRaise.() -> A): Option<A> =
fold({ block(OptionRaise(this)) }, ::identity, ::Some)

public inline fun <E, A> ior(noinline combineError: (E, E) -> E, @BuilderInference block: IorRaise<E>.() -> A): Ior<E, A> {
contract { callsInPlace(block, EXACTLY_ONCE) }
val state: Atomic<Option<E>> = Atomic(None)
return fold<E, A, Ior<E, A>>(
{ block(IorRaise(combineError, state, this)) },
Expand All @@ -56,10 +45,9 @@ public inline fun <E, A> ior(noinline combineError: (E, E) -> E, @BuilderInferen
public typealias Null = Nothing?

@JvmInline
public value class NullableRaise(private val cont: Raise<Null>) : Raise<Null> {
public value class NullableRaise(private val raise: Raise<Null>) : Raise<Null> by raise {
@RaiseDSL
public fun ensure(value: Boolean): Unit = ensure(value) { null }
override fun raise(r: Nothing?): Nothing = cont.raise(r)
public fun <B> Option<B>.bind(): B = getOrElse { raise(null) }

public fun <B> B?.bind(): B {
Expand All @@ -74,14 +62,12 @@ public value class NullableRaise(private val cont: Raise<Null>) : Raise<Null> {
}

@JvmInline
public value class ResultRaise(private val cont: Raise<Throwable>) : Raise<Throwable> {
override fun raise(r: Throwable): Nothing = cont.raise(r)
public value class ResultRaise(private val raise: Raise<Throwable>) : Raise<Throwable> by raise {
public fun <B> Result<B>.bind(): B = fold(::identity) { raise(it) }
}

@JvmInline
public value class OptionRaise(private val cont: Raise<None>) : Raise<None> {
override fun raise(r: None): Nothing = cont.raise(r)
public value class OptionRaise(private val raise: Raise<None>) : Raise<None> by raise {
public fun <B> Option<B>.bind(): B = getOrElse { raise(None) }
public fun ensure(value: Boolean): Unit = ensure(value) { None }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
@file:JvmMultifileClass
@file:JvmName("RaiseKt")
@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class)

package arrow.core.raise

import arrow.atomic.AtomicBoolean
import arrow.core.nonFatalOrThrow
import arrow.core.Either
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.coroutines.cancellation.CancellationException
import kotlin.experimental.ExperimentalTypeInference
Expand Down Expand Up @@ -75,7 +76,6 @@ public inline fun <R, A, B> fold(
transform: (value: A) -> B,
): B {
contract {
callsInPlace(program, EXACTLY_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
}
Expand All @@ -94,7 +94,7 @@ public inline fun <R, A, B> fold(
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
}
val raise = DefaultRaise()
val raise = DefaultRaise(false)
return try {
val res = program(raise)
raise.complete()
Expand All @@ -108,27 +108,91 @@ public inline fun <R, A, B> fold(
}
}

/**
* Inspect a [Trace] value of [R].
*
* Tracing [R] can be useful to know where certain errors, or failures are coming from.
* Let's say you have a `DomainError`, but it might be raised from many places in the project.
*
* You would have to manually _trace_ where this error is coming from,
* instead [Trace] offers you ways to inspect the actual stacktrace of where the raised value occurred.
*
* Beware that tracing can only track the [Raise.bind] or [Raise.raise] call that resulted in the [R] value,
* and not any location of where the [R], or [Either.Left] value was created.
*
* ```kotlin
* public fun main() {
* val error = effect<String, Int> { raise("error") }
* error.traced { (trace, _: String) -> trace.printStackTrace() }
* .fold({ require(it == "error") }, { error("impossible") })
* }
* ```
* ```text
* arrow.core.continuations.RaiseCancellationException: Raised Continuation
* at arrow.core.continuations.DefaultRaise.raise(Fold.kt:77)
* at MainKtKt$main$error$1.invoke(MainKt.kt:6)
* at MainKtKt$main$error$1.invoke(MainKt.kt:6)
* at arrow.core.continuations.Raise$DefaultImpls.bind(Raise.kt:22)
* at arrow.core.continuations.DefaultRaise.bind(Fold.kt:74)
* at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Traced.kt:46)
* at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Traced.kt:46)
* at arrow.core.continuations.Effect__FoldKt.fold(Fold.kt:92)
* at arrow.core.continuations.Effect.fold(Unknown Source)
* at MainKtKt.main(MainKt.kt:8)
* at MainKtKt.main(MainKt.kt)
* ```
*
* NOTE:
* This implies a performance penalty of creating a stacktrace when calling [Raise.raise],
* but **this only occurs** when composing `traced`.
* The stacktrace creation is disabled if no `traced` calls are made within the function composition.
*/
@ExperimentalTraceApi
public inline fun <R, A> Raise<R>.traced(
@BuilderInference program: Raise<R>.() -> A,
trace: (traced: Trace, R) -> Unit
): A {
val isOuterTraced = this is DefaultRaise && isTraced
val nested = if (this is DefaultRaise && isTraced) this else DefaultRaise(true)
return try {
program.invoke(nested)
} catch (e: RaiseCancellationException) {
val r: R = e.raisedOrRethrow(nested)
trace(Trace(e), r)
if (isOuterTraced) throw e else raise(r)
}
}

/** Returns the raised value, rethrows the CancellationException if not our scope */
@PublishedApi
@Suppress("UNCHECKED_CAST")
internal fun <R> CancellationException.raisedOrRethrow(raise: DefaultRaise): R =
if (this is RaiseCancellationException && this.raise === raise) raised as R
else throw this
when {
this is RaiseCancellationExceptionNoTrace && this.raise === raise -> raised as R
this is RaiseCancellationException && this.raise === raise -> raised as R
else -> throw this
}

/** Serves as both purposes of a scope-reference token, and a default implementation for Raise. */
@PublishedApi
internal class DefaultRaise : Raise<Any?> {
internal class DefaultRaise(@PublishedApi internal val isTraced: Boolean) : Raise<Any?> {
private val isActive = AtomicBoolean(true)

@PublishedApi
internal fun complete(): Boolean = isActive.getAndSet(false)
override fun raise(r: Any?): Nothing =
if (isActive.value) throw RaiseCancellationException(r, this) else throw RaiseLeakedException()
override fun raise(r: Any?): Nothing = when {
isActive.value -> throw if (isTraced) RaiseCancellationException(r, this) else RaiseCancellationExceptionNoTrace(r, this)
else -> throw RaiseLeakedException()
}
}

/** CancellationException is required to cancel coroutines when raising from within them. */
private class RaiseCancellationException(val raised: Any?, val raise: Raise<Any?>) : CancellationExceptionNoTrace()
private class RaiseCancellationExceptionNoTrace(val raised: Any?, val raise: Raise<Any?>) :
CancellationExceptionNoTrace()

public expect open class CancellationExceptionNoTrace() : CancellationException
private class RaiseCancellationException(val raised: Any?, val raise: Raise<Any?>) : CancellationException()

internal expect open class CancellationExceptionNoTrace() : CancellationException

private class RaiseLeakedException : IllegalStateException(
"""
Expand All @@ -138,3 +202,8 @@ private class RaiseLeakedException : IllegalStateException(
See: Effect documentation for additional information.
""".trimIndent()
)

internal const val RaiseCancellationExceptionCaptured: String =
"kotlin.coroutines.cancellation.CancellationException should never get cancelled. Always re-throw it if captured." +
"This swallows the exception of Arrow's Raise, and leads to unexpected behavior." +
"When working with Arrow prefer Either.catch or arrow.core.raise.catch to automatically rethrow CancellationException."
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName


/**
* Accumulate the errors from running both [action1] and [action2] using the given [combine] function.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@file:JvmMultifileClass
@file:JvmName("RaiseKt")
package arrow.core.raise

import kotlin.coroutines.cancellation.CancellationException
import kotlin.jvm.JvmInline
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

@RequiresOptIn("This API is experimental, and may change in the future.")
public annotation class ExperimentalTraceApi

/** Tracing result. Allows to inspect the traces from where raise was called. */
@ExperimentalTraceApi
@JvmInline
public value class Trace(private val exception: CancellationException) {
/**
* Returns the stacktrace as a [String]
*
* Note, the first line in the stacktrace will be the `RaiseCancellationException`.
* The users call to `raise` can found in the_second line of the stacktrace.
*/
public fun stackTraceToString(): String = exception.stackTraceToString()

/**
* Prints the stacktrace.
*
* Note, the first line in the stacktrace will be the `RaiseCancellationException`.
* The users call to `raise` can found in the_second line of the stacktrace.
*/
public fun printStackTrace(): Unit =
exception.printStackTrace()

/**
* Returns the suppressed exceptions that occurred during cancellation of the surrounding coroutines,
*
* For example when working with `Resource`, or `bracket`:
* When consuming a `Resource` fails due to [Raise.raise] it results in `ExitCase.Cancelled`,
* if the finalizer then results in a `Throwable` it will be added as a `suppressedException` to the [CancellationException].
*/
public fun suppressedExceptions(): List<Throwable> =
exception.suppressedExceptions
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class StructuredConcurrencySpec : StringSpec({
}.fold(::identity) { fail("Should never be here") } shouldBe "hello"

withTimeout(2.seconds) {
cancelled.await().shouldNotBeNull().message shouldBe "Raised Continuation"
cancelled.await().shouldNotBeNull().message shouldBe RaiseCancellationExceptionCaptured
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package arrow.core.raise

import arrow.core.right
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.string
import io.kotest.property.checkAll
import kotlinx.coroutines.CompletableDeferred

@OptIn(ExperimentalTraceApi::class)
class TraceSpec : StringSpec({
"trace is empty when no errors" {
checkAll(Arb.int()) { i ->
either<Nothing, Int> {
traced({ i }) { _,_ -> unreachable() }
} shouldBe i.right()
}
}

"trace is empty with exception" {
checkAll(Arb.string()) { msg ->
val error = RuntimeException(msg)
shouldThrow<RuntimeException> {
either<Nothing, Int> {
traced({ throw error }) { _,_ -> unreachable() }
}
}.message shouldBe msg
}
}

"nested tracing - identity" {
val inner = CompletableDeferred<String>()
ior(String::plus) {
traced({
traced({ raise("") }) { traced, _ ->
inner.complete(traced.stackTraceToString())
}
}) { traced, _ ->
inner.await() shouldBe traced.stackTraceToString()
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package arrow.core.raise

import kotlin.coroutines.cancellation.CancellationException

public actual open class CancellationExceptionNoTrace : CancellationException("Raised Continuation")
internal actual open class CancellationExceptionNoTrace : CancellationException(RaiseCancellationExceptionCaptured)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlin.coroutines.cancellation.CancellationException
* Inspired by KotlinX Coroutines:
* https://github.com/Kotlin/kotlinx.coroutines/blob/3788889ddfd2bcfedbff1bbca10ee56039e024a2/kotlinx-coroutines-core/jvm/src/Exceptions.kt#L29
*/
public actual open class CancellationExceptionNoTrace : CancellationException("Raised Continuation") {
internal actual open class CancellationExceptionNoTrace : CancellationException(RaiseCancellationExceptionCaptured) {
override fun fillInStackTrace(): Throwable {
// Prevent Android <= 6.0 bug.
stackTrace = emptyArray()
Expand Down
Loading

0 comments on commit 0a86ec2

Please sign in to comment.