Is there a pattern for safe resource handover from...
# coroutines
f
Is there a pattern for safe resource handover from ‘async’ to the enclosing scope?
k
Can you clarify your question with an example snippet?
f
For example:
Copy code
fun allocateResource(): Closeable = TODO()
fun setupAndCreateResource(): Closeable{
    //Do something
    var a: Closeable? = null
    try{
        a = allocateResource()
        doSmth()
    }
    catch(e: Throwable){
        try {
            a?.close()
        } catch (c:Exception){
            e.addSuppressed(c)
        }
        throw e
    }
    return a
}

suspend fun smth() = coroutineScope { 
    try {
        val defferedRes = async {
            setupAndCreateResource()
        }
        // Bug: will not dispose resource if allocated during cancel
        defferedRes.await().use { 
            
        }
    }
}
Basically need the simplest way to ensure the behavior:
Copy code
If the producer of the value returns a resource then the consumer is guaranteed to receive it, or if not able to receive it then the resource is guaranteed to be disposed
c
What do you think of https://arrow-kt.io/learn/coroutines/resource-safety/? Can be discussed in #arrow
f
@CLOVIS I’ve looked at it before. I’m not sure it’s meant for producer consumer handoff
c
I'm not sure, but I think it could be used for that? Maybe ask in #arrow with your use case
k
I can think of a few things: 1. Since a
Deferred
is a
Job
you could install a cleanup handler with
Job#invokeOnCompletion
to clean up that resource. 2. You could wrap the contents of the first
async
block in
withContext(NonCancellable)
3. You could decouple the actual creation of the closeable resource and the possibility for it to be cancelled. (Right now you don’t have it marked as
suspend
but since you’ve got it wrapped in an async I assume that
doSmth()
is a suspend fun).
f
1. I think this might , but will require passing the parent job to the child so it can register it.
I’m not sure 2/3 would work since async could complete successfully but parent could be cancelled on await()…
k
3. should always work since lifting the creation of the closeable resource up and calling
use
on it is essentially a call to
finally { close() }
. Finally blocks get called on cancellation.
f
I’m probably misunderstanding #3. Could you elaborate?
Normally I want to do other stuff between async and wait for deferred. ofc in this case it’s meaningless to make it async, it’s just for the sake of example
k
Restructure your code to be something like the following:
Copy code
suspend fun foo() = coroutineScope {
  val resource = setupResourceSynchronous()
  resource.use { r -> 
    val thing1 = async { r.thing1() }
    val thing2 = async { r.thing2() }
    return thing1.await() + thing2.await()
  }
}
The resource will always be cleaned up properly with that structure
if you need an async value to create the resource, lift it out of the actual creation and add it as a parameter
Copy code
suspend fun foo() = coroutineScope {
  val someAsyncValue = somethingAsync() // suspends
  val resource = setupResourceSynchronous(someAsyncValue)
  resource.use { r -> 
    val thing1 = async { r.thing1() }
    val thing2 = async { r.thing2() }
    return thing1.await() + thing2.await()
  }
}