https://kotlinlang.org logo
Title
f

fitermay

05/14/2023, 4:59 PM
Is there a pattern for safe resource handover from ‘async’ to the enclosing scope?
k

kevin.cianfarini

05/14/2023, 5:21 PM
Can you clarify your question with an example snippet?
f

fitermay

05/15/2023, 2:44 AM
For example:
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:
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

CLOVIS

05/15/2023, 8:47 AM
What do you think of https://arrow-kt.io/learn/coroutines/resource-safety/? Can be discussed in #arrow
f

fitermay

05/15/2023, 11:04 AM
@CLOVIS I’ve looked at it before. I’m not sure it’s meant for producer consumer handoff
c

CLOVIS

05/15/2023, 11:39 AM
I'm not sure, but I think it could be used for that? Maybe ask in #arrow with your use case
k

kevin.cianfarini

05/15/2023, 12:43 PM
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

fitermay

05/15/2023, 12:46 PM
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

kevin.cianfarini

05/15/2023, 12:50 PM
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

fitermay

05/15/2023, 12:52 PM
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

kevin.cianfarini

05/15/2023, 1:27 PM
Restructure your code to be something like the following:
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
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()
  }
}