-
Notifications
You must be signed in to change notification settings - Fork 449
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
Conversation
Kover Report
|
arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt
Show resolved
Hide resolved
arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt
Show resolved
Hide resolved
return finalizers.get().asReversed().fold(error) { acc, function -> | ||
acc.add(runCatching { function.invoke(error) }.exceptionOrNull()) | ||
fun close(error: Throwable?) { | ||
finalizers.value.asReversed().fold(error) { acc, finalizer -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to get the code closer to the style of the corresponding resource scope implementation, mainly for the sake of future readers of the code (since in essence the two are very similar)
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Outdated
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Outdated
Show resolved
Hide resolved
}?.let { throw it } | ||
} | ||
return Pair(allocated, release) | ||
public suspend fun <A> Resource<A>.allocated(): Pair<A, suspend (ExitCase) -> Unit> = with(ResourceScopeImpl()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous code didn't correctly add up the suppressed exceptions into the new one for some reason (at least that reason isn't clear to me), so I changed it (and added the relevant test) to match the semantics of resourceScope
val finalizer: suspend (ExitCase) -> Unit = { ex: ExitCase -> release(a, ex) } | ||
finalizers.update(finalizer::prependTo) | ||
a | ||
}, ::identity, { a, ex -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how to describe the previous code here other than that it clearly was refactored slowly until it rotted away. Basically, because the use
function passed to bracketCase
is identity
, ex
is always Completed
, and hence the code in that finalizer is a no-op.
is ExitCase.Failure -> exitCase.failure | ||
} | ||
release(a, errorOrNull) | ||
suspend fun cancelAll(exitCase: ExitCase) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one was also refactored to look more like the auto-close impl, mainly by adding that Throwable?.add
extension which cleans things up nicely
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wary of removing NonCancellable
from install
- will that allow for a resource to be instantiated and cancellation to occur before a finalizer is registered, and therefore left dangling?
Also it may be worth including similar test/fixes to bracket
and guarantee
around the non-local return / finally in the same PR as an overhaul of all related constructs. Especially as when fixed they'll all likely follow the same approach of "acquire a scope, action the scope and the finalize exactly once on either error or completion" but potentially with different semantics on exception handling/throwing.
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt
Outdated
Show resolved
Hide resolved
arrow-libs/core/arrow-autoclose/src/jvmMain/kotlin/arrow/throwIfFatal.kt
Outdated
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt
Show resolved
Hide resolved
@OptIn(DelicateCoroutinesApi::class) | ||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") | ||
public suspend inline fun <A> resourceScope(action: suspend ResourceScope.() -> A): A { | ||
val (scope, cancelAll) = resource { this }.allocated() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little hacky, but it results in no new ABI added. We might instead want a function allocateResourceScope(): Pair<ResourceScope, suspend (ExitCase) -> Unit>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine - if anything a comment to highlight that this
is the ResourceScope
for clarity but it's internal and imo understandable from the context.
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if my approval counts for much, but I think this is all much improved, leaner and more composed.
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
@OptIn(DelicateCoroutinesApi::class) | ||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") | ||
public suspend inline fun <A> resourceScope(action: suspend ResourceScope.() -> A): A { | ||
val (scope, cancelAll) = resource { this }.allocated() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine - if anything a comment to highlight that this
is the ResourceScope
for clarity but it's internal and imo understandable from the context.
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt
Show resolved
Hide resolved
return install({ a }, release) | ||
} | ||
public suspend infix fun <A> Resource<A>.releaseCase(release: suspend (A, ExitCase) -> Unit): A = | ||
bind().also { a -> onRelease { release(a, it) } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to be concerned about bind()
being called in a non-cancellable context here? (It wasn't previously, either) - possibly a question for @nomisRev ?
It does mean resource { ... } release {}
acts differently to install({ ... }) { _, _ -> }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resource
is basically a fancy name for any function that you might call taking a ResourceScope
, so I'd assume that either the function registers its own release (and this one here is extra for some reason?) or that the function registers some resources, but it leaves the final one for you to register yourself.
Inline usage of resource
like that is stylistically tempting, but it feels easier to just do it imperatively.
Maybe what we actually want is for resource
to be non-cancellable by default?
This covers what #3515 set out to achieve, with the same sureties on finalizers being called with non-local returns, but also simplifies and shares the previously-duplicated (and subtly different) implementations of |
Hey @serras, I've taken the only binary-breaking change out of this PR and into #3522. Thus, this PR can wait until some 2.x version, so you can take your time with it! |
Remove unnecessary suppressions for usages of AutoClosable Force closing on non-local returns
7373491
to
8131f37
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the hard work on this!
Apart from a few clarifications, my main concern here is that some tests change their behavior, and we need to be 100% sure that this is OK.
arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt
Outdated
Show resolved
Hide resolved
arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt
Outdated
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/BracketCaseTest.kt
Show resolved
Hide resolved
@kyay10 sorry, I created some conflicts by merging the other two open PRs... |
No problem with the conflicts! They should be trivial to fix up |
arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt
Show resolved
Hide resolved
arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt
Outdated
Show resolved
Hide resolved
# Conflicts: # arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api # arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.klib.api # arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt # arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt
} | ||
} catch (e: Throwable) { | ||
e.nonFatalOrThrow() | ||
when (ex) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be written as (ex.errorOrNull ?: throw e).addSuppressed(e)
but I went for clarity instead, so that the code now reads like "if fa completed successfully, rethrow the error from its finalizer. Otherwise, suppress that error"
This subsumes #3515. This PR can be changed to use that same approach (or if #3515 is merged, this PR can just directly use it).
I've avoided source-breaking changes here. I would've liked to make
autoClose
inline, but that'd be source-breaking.I changed the required methods to implement for
AutoCloseScope
andResourceScope
, but does Arrow consider that part of its source compatibility? I'm assuming not.Compiler complains about
resourceScope
anduse
not needing an explicitlysuspend
block anymore (because it's automatically passed-through byinline
). However, I think that change can be source-breaking if someone was using a function reference (compiler isn't smart enough to understand that sadly), so I suppressed that warning (which is the approach taken inBracket
as well)I caught an error where
autoCloseScope
wouldn't close if an early-return happened. That was easily solvable usingfinally
(which I think covers every possible exit scenario? ExceptsuspendCoroutine<Nothing> { }
but there's no way to stop that, and it's basically like looping forever). I fixed that error for Bracket-like functions as well, and added tests for the aforementioned and foruse
andresourceScope
I found a logical error where
allocated
didn't have the same semantics asresourceScope
when it came to adding suppressed errors. Nowallocated
is very dumb and clearly has the same semantics, but I still added a test case anyway.The code uses some
also
s but they're used whenever the original code used them. However, I'd be happy to convert them to imperative style if you wish!Some tests for bracket composing weren't testing suppressed exceptions correctly, and fixing that revealed a problem where
throwable
s were reused across tests, which doesn't work with the mutation ofsuppressedExceptions
To discuss: do we need AcquireStep still? I recall a discussion with Simon (TODO link) about where it came from (some Scala lib) and how it's likely unnecessary anymore. We can even keep it but deprecate it and remove the DSL annotation from it so its restrictions don't apply anymore