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,Fguarantees 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 returnedAis 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