phldavies
09/16/2022, 3:04 PMCoroutineScope
to avoid any leaked resources if the finaliser isn’t called. I’m not sure if this is entirely safe wrt structured concurrency but it passes some rudimentary tests (and finalizer is called within the current context):
context(CoroutineScope)
suspend fun <A> Resource<A>.bind(): Pair<A, suspend () -> Unit> =
allocate().let { (acquired, release) ->
acquired to guaranteeCase(release)
}
fun CoroutineScope.guaranteeCase(finalizer: suspend (ExitCase) -> Unit): suspend () -> Unit {
val completion = CompletableDeferred<Unit>()
val finalize = async(NonCancellable) { guaranteeCase(completion::await, finalizer) }
val hookHandle = coroutineContext.job.invokeOnCompletion {
if (it != null) completion.completeExceptionally(it)
else completion.complete(Unit)
}
return {
hookHandle.dispose()
completion.complete(Unit)
finalize.await()
}
}
or alternatively without the CoroutineScope
receiver/context (but requiring a current Job
)
suspend fun <A> Resource<A>.bind(): Pair<A, suspend () -> Unit> =
allocate().let { (acquired, release) ->
acquired to guaranteeCase(release)
}
suspend fun guaranteeCase(finalizer: suspend (ExitCase) -> Unit): suspend () -> Unit {
val completion = CompletableDeferred<Unit>()
val finalize = async(NonCancellable) { guaranteeCase(completion::await, finalizer) }
val hookHandle = coroutineContext.job.invokeOnCompletion {
if (it != null) completion.completeExceptionally(it)
else completion.complete(Unit)
}
return {
hookHandle.dispose()
completion.complete(Unit)
finalize.await()
}
}
private suspend fun async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) = Job(coroutineContext.job).let { job ->
CoroutineScope(coroutineContext + job)
.async(context, start, block)
.also { job.complete() }
}
simon.vergauwen
09/16/2022, 3:57 PMIOApp
, and I use this in combination with ResourceScope
to achieve the same behavior but through Resource
.
suspend fun ResourceScope.dependency(): String = Resource({ "Dependency" }) { a, exitCase -> }
fun main() = SuspendApp {
resource {
dependency()
}.use { awaitCancellation() }
}
simon.vergauwen
09/16/2022, 3:58 PMResource
if you have ResourceScope
in your context.
The reason for doing this is so that you can compose the handlers correctly throughout the entire ResourceScope
however deeply it may be nested.simon.vergauwen
09/16/2022, 4:01 PMCoroutineScope
is (safely) possible 🤔
Relying on coroutineContext.job.invokeOnCompletion
is not safe in all cases, since it heavily depends on where it's called, and due to the implicit nature of Job
and CoroutineContext
there is no type-safety to protect your from incorrect usage.phldavies
09/16/2022, 4:03 PMsimon.vergauwen
09/16/2022, 4:04 PMResource
?simon.vergauwen
09/16/2022, 4:05 PMphldavies
09/16/2022, 4:06 PMinvokeOnCompletion
should always be invoked but there’s no guarantee about the scope/thread of the invocation - which is why I only used it to signal via the CompletableDeferred
which is being waited on in the correct scope.simon.vergauwen
09/16/2022, 4:10 PMSuspendApp
stable, it was originally developed in production for JVM.
While looking for a solution I also played with job.coroutineContext
but it was super delicate if you make any any code changes. The same patterns might apply depending on usage. invokeOnCompletion
will always be called, but when implicitly depends on call-site.phldavies
09/16/2022, 4:12 PMIf I understood correctly, being able to wrapped unwrapped code forMy intent here was to see if we can provide some level of guarantee that a manually?Resource
allocated
resource is finalised at some point given the structured concurrency inherent in kotlin’s coroutines. Mainly to provide some level of parity with cats.effect.Resource.allocated
that at least guarantees finalisation on interrupt/failure if not for completion.simon.vergauwen
09/19/2022, 6:30 AMthat at least guarantees finalisation on interrupt/failure if not for completion.Allocated makes no such guarantees? Once the resource is allocated the responsibility is 100% on the user to call
release
in all 3 of the cases. Completion, failure or cancellation.phldavies
09/19/2022, 10:20 AMsimon.vergauwen
09/19/2022, 11:32 AMIf the outerfails or is interrupted,F
guarantees that the finalizers willallocated
be called. However, if the outersucceeds, it's up to the user to ensure the returnedF
is called onceF[Unit]
needs to be released. If the returnedA
is not called,F[Unit]
the finalizers will not be run.Can be rephrased, If
allocated
fails or is interrupted then allocated
guarantees calling the finalizers of the already allocated resources within the composed Resource. If allocating the resource succeeds then the responsibility falls unto the user to call the finalizers.
The semantics is the following, and this is the same for Arrow and Cats-effect, and is also why use is recommended by Cats (& Arrow).
Let's say you have a Resource<Persistence>
and it exists out of DataSoure
and SqlDelight
for example. Then we do
val dataSource: Resource<DataSource> = TODO()
fun sqlDelight(dataSource: DataSource): Resource<SqlDelight> = TODO()
fun Persistence(val sqlDelight: SqlDelight): Resource<Persistence> = TODO()
val persistence = resource {
val ds = dataSource.bind()
val sqlDelight = sqlDelight(ds).bind()
Persistence(sqlDelight).bind()
}
Let's say we use the same signature we discussed above, so the same as Cats-effect. Making the types concrete it becomes:
suspend fun Resource<Persistence>.allocated(): Pair<Persistence, suspend (ExitCase) -> Unit>
If this function returns, then the caller is responsibly of calling suspend (ExitCase) -> Unit
in the case the user-land-code fails, cancels or completes. It's not possible to call that function based on any failures or cancellations from user-land.simon.vergauwen
09/19/2022, 11:35 AMallocated
operator of Cats-effect is incorrect, and that is also why I marked it as @DelicateCoroutineApi
in Arrow Fx Coroutines. It should only be used from low-level library code.simon.vergauwen
09/19/2022, 11:36 AMResource
to a Job
but it feels error-prone to me. In that regard it's probably much safer to use ResourceScope
as a marker type to wire this capability in a type-safe way.phldavies
09/19/2022, 11:43 AMsimon.vergauwen
09/19/2022, 11:44 AM