Youssef Shoaib [MOD]
05/17/2024, 11:06 PMAcquireStep
needs to exist? I'm not seeing the issue with bind
being called inside the acquire. What would be the issue with that? The resource that was bound inside will have its finalizer also updated and everything, and as long as it completes successfully, the current finalizer will also be added to the list. Is there something I'm missing?simon.vergauwen
05/18/2024, 6:48 AMsimon.vergauwen
05/18/2024, 6:56 AMManagedT
from Haskell, and Resource
from Cats-effect. The problem is a compositional one, the acquire
step is uncancellable, since otherwise we can break the composed function.
See https://github.com/typelevel/cats-effect/issues/382.
However, I am not longer entirely sure it's required in Kotlin. In Kotlin our resource will almost never be build out of IO
with flatMap
, and KotlinX doesn't automatically add cancellation boundaries. So I see this as less problematic.
Some scenario could be where someone adds a Schedule around the acquire step, but then I am not sure why it should not be cancelled. There are most likely not going to be cancellation boundaries in the "setup" part of the acquisition.
As a matter of fact we purpose to keep the acquire step small.
cc\\ @Alexandru Nedelcu you were part of a lot of these discussions in Scala, I would love your thoughts here! π (Sorry for the random tag).
Also calling @Alejandro Serrano.Mena, not sure how familiar you are with ManagedT and how it works with IO in relation to cancellation/interruption but I'd love your thoughts as well.
PS: Great work in the PR @Youssef Shoaib [MOD], but this change is something that I rather wait with until after KotlinConf so I can properly wrap my head around it π
Alexandru Nedelcu
06/03/2024, 11:06 AMacquire
should be uncancellable by default, however in Cats-Effect 3, the developer does have a builder to describe cancellation logic for acquire, as well. This would be needed, for example, in the case of a "blocking queue" or a "semaphore", where the waiting part should be cancellable.
Kotlin does not have cancellation boundaries, except when it does π
For example:
val queue = new ArrayBlockingQueue[Resource]()
// acquisition
val res = runInterruptible {
queue.take()
} // <- cancellation boundary here
try {
doStuff(res)
} finally {
// release
queue.offer(res)
}
Unfortunately, this can lead to a leak, as Kotlin does in fact introduce an implicit cancellation boundary.simon.vergauwen
06/03/2024, 11:18 AMacquire
behavior from CE.
Kotlin does not have cancellation boundaries, except when it does πGood point about
runInterruptible
because, NonCancellable
doesn't do that.
withContext(NonCancellable) {
} // <-- no cancellation boundary
Which makes it even more tricky I guess, since this is fine:
val acquired = withContext(NonCancellable) {
acquire()
} // <-- no cancellation boundary
closing { e: Throwable? -> acquired.close(e) }
the developer does have a builder to describe cancellation logic for acquireHmm, I think I missed that. Is the new type for acquire
Poll[F] => A
instead of => A
? There is a Poll
implementation for Kotlin, which is still useful in special scenarios like this.
I'm wondering if we need to rethink the primitives here for Kotlin to better match KotlinX Coroutines cooperative behavior, but operators like runInterruptible
indeed make things trickier.Alexandru Nedelcu
06/03/2024, 1:10 PMPoll[F] => A
, which is part of the uncancellable
operator that's now the primitive. For Cats-Effect, it works out well.
If you make acquire
non-cancellable, that's good enough for most cases. Again, we're talking about modeling a resource whose acquisition can be interrupted. And there are some use-cases for that, such as getting a resource from a resource-pool.
The runInterruptible
example can be fixed by capturing the resource in a var
.
val queue = new ArrayBlockingQueue[Connection]()
var res: Connection? = null
try {
// acquisition
runInterruptible {
res = queue.take()
}
// usage part
use(res)
} finally {
// release
if (res != null) queue.offer(res)
}
Alexandru Nedelcu
06/03/2024, 1:11 PMAlexandru Nedelcu
06/03/2024, 1:17 PMYoussef Shoaib [MOD]
06/03/2024, 1:19 PMinstall
? Perhaps the key thing here is that one should just onRelease {...}
right when a resource is readysimon.vergauwen
06/03/2024, 1:31 PMval queue = new ArrayBlockingQueue[Connection]()
val res = runInterruptible {
val res = queue.take().also { res ->
onRelease { _: Throwable? -> queue.offer(res) }
}
use(res)
There is no cancellation boundary between take
and onRelease
here.Alexandru Nedelcu
06/03/2024, 1:37 PMonRelease
function I found is a suspending function, and one problem would be that the function passed in runInterruptible
is not suspending.simon.vergauwen
06/03/2024, 1:38 PMAlexandru Nedelcu
06/03/2024, 1:39 PMsimon.vergauwen
06/03/2024, 1:40 PMonRelease
as Youssef proposed in a Resource improvement, https://github.com/arrow-kt/arrow/pull/3431 is no longer suspended.
I'm considering a more generalised DSL, https://github.com/arrow-kt/arrow/pull/3443, but it comes with some other issues like what if I want to close resources, but keep other finalisers untouched. This introduces a new kernel DSL module, and leaves the original Fx module untouched. Avoiding breaking compatibility as well πsimon.vergauwen
06/03/2024, 1:43 PMRaiseAccumulate
. Since extension receiver can be refactored to context parameter in a binary compat way, although not sure if we have to with single receivers.
This way we can split off the DSL, and leave everything as is. The only thing it might require additionally is imports.., which could be automatically inserted by the IntellIJ plugin, but it still breaks compatibility.
> Personally, whenever Cats-Effect broke compatibility, I hated it for it
I wonder if I should just stop, and leave Arrow as is to avoid breaking compatibility... I understand this sentiment π
Alexandru Nedelcu
06/03/2024, 1:44 PMAlexandru Nedelcu
06/03/2024, 1:45 PMsimon.vergauwen
06/03/2024, 1:45 PMonRelease
is non-suspend, but the lambda is suspend.simon.vergauwen
06/03/2024, 1:46 PMonRelease
is essentially just registering the lambda
into an AtomicReference
.Alexandru Nedelcu
06/03/2024, 1:46 PMAlexandru Nedelcu
06/03/2024, 1:47 PMsimon.vergauwen
06/03/2024, 1:47 PMsimon.vergauwen
06/03/2024, 1:47 PMwithContext(NonCancellable) { }
Alexandru Nedelcu
06/03/2024, 1:47 PMsimon.vergauwen
06/03/2024, 1:48 PMsimon.vergauwen
06/03/2024, 1:48 PMresource(acquire, release)
and an interruptible variant.Alexandru Nedelcu
06/03/2024, 1:49 PMAlexandru Nedelcu
06/03/2024, 1:49 PMsimon.vergauwen
06/03/2024, 1:50 PMAlexandru Nedelcu
06/03/2024, 1:53 PMResource.make
, Resource.apply
or Resource.makeCase
from Cats-Effect are very much like the current install
in Arrow. Non-cancellable, and does the right thing in 80%+ of cases.
Then you have Resource.makeFull
, Resource.makeCaseFull
, and Resource.applyFull
giving you Poll
for more control.Alexandru Nedelcu
06/03/2024, 1:54 PMinstallFull
or something.Alexandru Nedelcu
06/03/2024, 1:55 PMonRelease
at precisely the right time.simon.vergauwen
06/03/2024, 2:10 PMPoll
, https://github.com/nomisRev/arrow-fx-coroutines-utils/blob/main/src/commonMain/kotlin/io/github/nomisrev/UncancellableRegion.kt.
but I'm not too excited about the naming, or the APIs π
Alexandru Nedelcu
06/03/2024, 2:15 PMrunInterruptible
which can catch one by surprise. I think runInterruptible
can be fixed, but isn't due to backwards-compatibility concerns. (I remember seeing an issue open for it, but can't find it now)Youssef Shoaib [MOD]
11/01/2024, 7:43 PMYoussef Shoaib [MOD]
11/01/2024, 7:58 PMinstall(acquire, release)
is just:
val a = withContext(NonCancellable) { acquire() }
onRelease { release(a, it) }
onRelease { release(a, it) }
So if one wants cancellable resource creation, it's as simple as:
val a = acquire()
onRelease { release(a, it) }
onRelease { release(a, it) }
Having cancellable release is a doable change too, but would have to be library-side.
And of course, if your resource involves multiple resources, this approach composes nicely and you can just add the releasers for each sub-resource.Youssef Shoaib [MOD]
11/01/2024, 7:59 PMuncancellable
caller then withContext(thatJob)
inside cancellable
. This should have the same effect as your direct continuation manipulation.Youssef Shoaib [MOD]
11/01/2024, 9:34 PMPoll
related things, I don't see any issue unifying the Full
and non-full APIs in Kotlin. We'd just need to change AcquireStep
to be like UncancellableRegion
(and perhaps remove that dsl marker restriction)
suspend fun <A> install(
acquire: suspend AcquireStep.() -> A,
release: suspend (A, ExitCase) -> Unit,
): A = uncancellable {
acquire(AcquireStep(this)).also { a -> onRelease { release(a, it) } }
}
and, in fact, we stay source-compatible!
That pattern though would likely be used directly by users instead of calling install
.