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.