Skip to content

Commit

Permalink
AutoClose and Resource cleanup (#3518)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyay10 authored Nov 8, 2024
1 parent 2ecbc14 commit fa2fa40
Show file tree
Hide file tree
Showing 19 changed files with 482 additions and 240 deletions.
3 changes: 3 additions & 0 deletions arrow-libs/core/arrow-autoclose/api/arrow-autoclose.api
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions arrow-libs/core/arrow-autoclose/api/arrow-autoclose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@

// Library unique name: <io.arrow-kt:arrow-autoclose>
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§<kotlin.Any?>}[0]
abstract fun onClose(kotlin/Function1<kotlin/Throwable?, kotlin/Unit>) // arrow/AutoCloseScope.onClose|onClose(kotlin.Function1<kotlin.Throwable?,kotlin.Unit>){}[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§<kotlin.Any?>}[0]
open fun <#A1: kotlin/AutoCloseable> install(#A1): #A1 // arrow/AutoCloseScope.install|install(0:0){0§<kotlin.AutoCloseable>}[0]
}

final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { // arrow/DefaultAutoCloseScope|null[0]
constructor <init>() // arrow/DefaultAutoCloseScope.<init>|<init>(){}[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§<kotlin.Any?>}[0]
final fun close(kotlin/Throwable?): kotlin/Nothing? // arrow/DefaultAutoCloseScope.close|close(kotlin.Throwable?){}[0]
final fun onClose(kotlin/Function1<kotlin/Throwable?, kotlin/Unit>) // arrow/DefaultAutoCloseScope.onClose|onClose(kotlin.Function1<kotlin.Throwable?,kotlin.Unit>){}[0]
}

final fun (kotlin/Throwable).arrow/throwIfFatal(): kotlin/Throwable // arrow/throwIfFatal|[email protected](){}[0]
Expand Down
1 change: 0 additions & 1 deletion arrow-libs/core/arrow-autoclose/knit.code.include
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package arrow

import arrow.atomic.Atomic
import arrow.atomic.update
import arrow.atomic.value
import kotlin.coroutines.cancellation.CancellationException

/**
Expand Down Expand Up @@ -63,50 +64,50 @@ import kotlin.coroutines.cancellation.CancellationException
*/
public inline fun <A> 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 <A> autoClose(
acquire: () -> A,
release: (A, Throwable?) -> Unit
): A
): A = acquire().also { a -> onClose { release(a, it) } }

@ExperimentalStdlibApi
public fun <A : AutoCloseable> 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 <A> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlinx.coroutines.test.runTest
import kotlin.coroutines.cancellation.CancellationException
import kotlin.test.Test

@OptIn(ExperimentalStdlibApi::class)
class AutoCloseTest {

@Test
Expand All @@ -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
}

Expand All @@ -42,16 +41,16 @@ class AutoCloseTest {
shouldThrow<RuntimeException> {
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
}

Expand All @@ -67,20 +66,20 @@ class AutoCloseTest {
val e = shouldThrow<RuntimeException> {
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
}

Expand All @@ -93,7 +92,7 @@ class AutoCloseTest {
val e = shouldThrow<RuntimeException> {
autoCloseScope {
autoClose({ Resource() }) { r, e ->
promise.complete(e)
require(promise.complete(e))
r.shutdown()
throw error2
}
Expand All @@ -102,7 +101,7 @@ class AutoCloseTest {
}
e shouldBe error
e.suppressedExceptions shouldBe listOf(error2)
promise.await() shouldBe error
promise.shouldHaveCompleted() shouldBe error
}

@Test
Expand All @@ -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
}

Expand All @@ -127,12 +126,29 @@ class AutoCloseTest {
shouldThrow<CancellationException> {
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<Boolean>()
val res = Resource()

run {
autoCloseScope {
val r = install(res)
require(wasActive.complete(r.isActive()))
return@run
}
}

wasActive.shouldHaveCompleted() shouldBe true
res.isActive() shouldBe false
}

Expand Down Expand Up @@ -172,7 +188,6 @@ class AutoCloseTest {
closed.cancel()
}

@OptIn(ExperimentalStdlibApi::class)
private class Resource : AutoCloseable {
private val isActive = AtomicBoolean(true)

Expand All @@ -188,4 +203,9 @@ class AutoCloseTest {
shutdown()
}
}

private suspend fun <T> CompletableDeferred<T>.shouldHaveCompleted(): T {
isCompleted shouldBe true
return await()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>()
var wasActive = false
val res = Resource()

shouldThrow<LinkageError> {
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<LinkageError> {
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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit fa2fa40

Please sign in to comment.