Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AutoClose and Resource cleanup #3518

Merged
merged 29 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bd9d967
Simplify AutoCloseScope interface to a single method.
kyay10 Oct 31, 2024
64a6525
Simplify ResourceScope interface to a single method.
kyay10 Nov 1, 2024
6021c8c
Auto-update API files
kyay10 Nov 1, 2024
2f88761
Undo non-source-compatible changes
kyay10 Nov 1, 2024
8ec5469
Remove extra receivers from `install` and `autoClose`
kyay10 Nov 1, 2024
73d0365
Auto-update API files
kyay10 Nov 1, 2024
3d567d1
Preserve source compat for ResourceScope.autoClosable
kyay10 Nov 1, 2024
758b9e3
Make `install` non-cancellable and add tests for it
kyay10 Nov 1, 2024
630c7b4
Make the PR diff a little nicer
kyay10 Nov 1, 2024
a755c4b
Fix non-local returns in Bracket.kt, and add relevant tests
kyay10 Nov 1, 2024
35b40f0
Correct throwIfFatal
kyay10 Nov 1, 2024
7eeb2c0
Fix composition checks in BracketCaseTest
kyay10 Nov 1, 2024
ad6ce04
Hide IODispatcher from ABI
kyay10 Nov 1, 2024
626d27b
Add test for error composition for a successful `resourceScope`
kyay10 Nov 1, 2024
8d1dd60
Call `finalizer` in one place in `finalizeCase`
kyay10 Nov 1, 2024
64d9850
Call `scope.close` in one place in `autoCloseScope`
kyay10 Nov 1, 2024
3a5346c
Short-circuit fatal resource release
kyay10 Nov 1, 2024
affbdb9
Add tests for short-circuiting fatal bracket
kyay10 Nov 1, 2024
212a0fb
Reduce usage of CompletableDeferred in tests
kyay10 Nov 1, 2024
8131f37
Make DefaultAutoCloseScope.close binary-compatible
kyay10 Nov 4, 2024
44656bc
Update API files
kyay10 Nov 4, 2024
8e8837e
Delete kotlin-js-store/yarn.lock
kyay10 Nov 5, 2024
52695f4
Revert "Delete kotlin-js-store/yarn.lock"
kyay10 Nov 5, 2024
f10318e
Return yarn.lock to pre-PR state
kyay10 Nov 5, 2024
6474503
Merge branch 'main' into kyay10/autoclose-resource-rehaul-try2
kyay10 Nov 5, 2024
5414813
Auto-update API files
kyay10 Nov 5, 2024
184927b
Inline runReleaseAndRethrow and simplify
kyay10 Nov 5, 2024
bdda97a
Use CompletableDeferred in tests and add shouldHaveCompleted
kyay10 Nov 6, 2024
5d55fd1
Merge branch 'main' into kyay10/autoclose-resource-rehaul-try2
serras Nov 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
kyay10 marked this conversation as resolved.
Show resolved Hide resolved
scope.close(e) ?: throw e
} catch (e: Throwable) {
scope.close(e.throwIfFatal()) ?: throw e
throwable = e
throw e
} finally {
kyay10 marked this conversation as resolved.
Show resolved Hide resolved
if (throwable !is CancellationException) throwable?.throwIfFatal()
scope.close(throwable)
}
}

public interface AutoCloseScope {
public fun onClose(release: (Throwable?) -> Unit)
serras marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -3,62 +3,60 @@ package arrow
import arrow.atomic.AtomicBoolean
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.toList
import kotlinx.coroutines.test.runTest
import kotlin.coroutines.cancellation.CancellationException
import kotlin.test.Test

@OptIn(ExperimentalStdlibApi::class)
class AutoCloseTest {

@Test
fun canInstallResource() = runTest {
val promise = CompletableDeferred<Throwable?>()
val wasActive = CompletableDeferred<Boolean>()
var throwable: Throwable? = RuntimeException("Dummy exception")
kyay10 marked this conversation as resolved.
Show resolved Hide resolved
var wasActive = false
val res = Resource()

autoCloseScope {
val r = autoClose({ res }) { r, e ->
promise.complete(e)
throwable = e
serras marked this conversation as resolved.
Show resolved Hide resolved
r.shutdown()
}
wasActive.complete(r.isActive())
wasActive = r.isActive()
}

promise.await() shouldBe null
wasActive.await() shouldBe true
throwable shouldBe null
wasActive shouldBe true
res.isActive() shouldBe false
}

@Test
fun canHandleWithFailingAutoClose() = runTest {
val promise = CompletableDeferred<Throwable?>()
val wasActive = CompletableDeferred<Boolean>()
var throwable: Throwable? = RuntimeException("Dummy exception")
var wasActive = false
val error = RuntimeException("BOOM!")
val res = Resource()

shouldThrow<RuntimeException> {
autoCloseScope {
val r = autoClose({ res }) { r, e ->
promise.complete(e)
throwable = e
r.shutdown()
}
wasActive.complete(r.isActive())
wasActive = r.isActive()
throw error
}
} shouldBe error

promise.await() shouldBe error
wasActive.await() shouldBe true
throwable shouldBe error
wasActive shouldBe true
res.isActive() shouldBe false
}

@Test
fun addsSuppressedErrors() = runTest {
val promise = CompletableDeferred<Throwable?>()
val wasActive = CompletableDeferred<Boolean>()
var throwable: Throwable? = RuntimeException("Dummy exception")
var wasActive = false
val error = RuntimeException("BOOM!")
val error2 = RuntimeException("BOOM 2!")
val error3 = RuntimeException("BOOM 3!")
Expand All @@ -67,33 +65,33 @@ class AutoCloseTest {
val e = shouldThrow<RuntimeException> {
autoCloseScope {
val r = autoClose({ res }) { r, e ->
promise.complete(e)
throwable = e
r.shutdown()
throw error2
}
autoClose({ Resource() }) { _, _ -> throw error3 }
wasActive.complete(r.isActive())
wasActive = r.isActive()
throw error
}
}

e shouldBe error
e.suppressedExceptions shouldBe listOf(error3, error2)
promise.await() shouldBe error
wasActive.await() shouldBe true
throwable shouldBe error
wasActive shouldBe true
res.isActive() shouldBe false
}

@Test
fun handlesAcquireFailure() = runTest {
val promise = CompletableDeferred<Throwable?>()
var throwable: Throwable? = RuntimeException("Dummy exception")
val error = RuntimeException("BOOM!")
val error2 = RuntimeException("BOOM 2!")

val e = shouldThrow<RuntimeException> {
autoCloseScope {
autoClose({ Resource() }) { r, e ->
promise.complete(e)
throwable = e
r.shutdown()
throw error2
}
Expand All @@ -102,37 +100,54 @@ class AutoCloseTest {
}
e shouldBe error
e.suppressedExceptions shouldBe listOf(error2)
promise.await() shouldBe error
throwable shouldBe error
}

@Test
fun canInstallAutoCloseable() = runTest {
val wasActive = CompletableDeferred<Boolean>()
var wasActive = false
val res = Resource()

autoCloseScope {
val r = install(res)
wasActive.complete(r.isActive())
wasActive = r.isActive()
}

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

@Test
fun closeTheAutoScopeOnCancellation() = runTest {
val wasActive = CompletableDeferred<Boolean>()
var wasActive = false
val res = Resource()

shouldThrow<CancellationException> {
autoCloseScope {
val r = install(res)
wasActive.complete(r.isActive())
wasActive = r.isActive()
throw CancellationException("BOOM!")
}
}.message shouldBe "BOOM!"

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

@Test
fun closeTheAutoScopeOnNonLocalReturn() = runTest {
var wasActive = false
val res = Resource()

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

wasActive shouldBe true
res.isActive() shouldBe false
}

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

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

Expand Down
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
Loading