https://kotlinlang.org logo
#coroutines
Title
# coroutines
a

ansman

02/21/2019, 4:27 PM
Is there any way to handle dropped return values from a suspending coroutine? Say I do this:
Copy code
fun processImage() {
  val file = createFile()
  try {
    // Do stuff with the file
  } finally {
    file.delete
  }
}

suspend fun createFile(): File = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
  return File.createTemporaryFile("foo", "temp")
}
There is a case where
createFile
has finished but
processImage
has not yet been resumed. If during this time the job is cancelled we’ll never get a chance to clean up the file. Is there a way around this?
u

uli

02/21/2019, 4:52 PM
Would this solve your issue?
Copy code
fun processImage() {
  val file : File? = null
  try {
    file = createFile()
    // Do stuff with the file
  } finally {
    file?.delete
  }
}
a

ansman

02/21/2019, 4:52 PM
No, it would have the same result since
createFile
never actually “returned” a file
u

uli

02/21/2019, 4:53 PM
Right. The
File
is only returned from the withContext lambda parameter, but not yet from createFile
That's ugly 🙂
a

ansman

02/21/2019, 4:55 PM
I could use the unconfined dispatcher I suppose but then I’d lose the dispatcher of the parent
u

uli

02/21/2019, 5:08 PM
Did you see that coming? Or did it fall on your feet?
I think the clean solution is to clean up in createFile. It is so ugly because the closing brace of withContext schedules. If it were explicit like in this code:
Copy code
suspend fun createFile(): File {
    var file : File? = null
    withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
        file = File.createTempFile(("foo", "temp")
    }
    yield()
    return file
}
it would be obviouse that you have to fix it like this:
Copy code
suspend fun createFile(): File {
    var file: File? = null
    try {
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            file = File.createTempFile(("foo", "temp")
        }
        yield()
    } catch (Exception e) {
        file.delete()
        throw e
    }
    return file
}
t

trathschlag

02/21/2019, 5:36 PM
How about this:
Copy code
suspend fun processImage() {
    useTemporaryFile { file ->
        // do stuff with the file
    }
}

suspend fun <R> useTemporaryFile(body: suspend (File) -> R) : R = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    val file = createTempFile("foo", "temp")
    try {
        withContext(Dispatchers.Default) {
            body(file)
        }
    } finally {
        file.delete()
    }
}
this should always delete the file
u

uli

02/21/2019, 6:10 PM
Almost brilliant. Except we want to go back to where we came from, not to the default dispatcher
Copy code
suspend inline fun <R> useTemporaryFile(block: suspend (File) -> R): R {
    var file: File? = null
    try {
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            file = createTempFile("foo", "temp")
        }
        return file?.let { block(it) } ?: throw IllegalStateException()
    } finally {
        file?.delete()
    }
}
If only `withContext`was already annotated with an appropriate contract, the compiler could probably smart cast
file
and we would not need the ugly `let`/`throw`
a

ansman

02/21/2019, 6:20 PM
This was just an example to illustrate the general problem, the creation of the resource cannot be called in a blocked way
u

uli

02/21/2019, 6:23 PM
understood. But i think @trathschlag's approach is one to remember for this general case
while writing my code above as an exercise i almost did the same mistake again. This issue is so subtile. The following code has the same, original problem:
Copy code
// Don't use! This code is broken
suspend inline fun <R> useTemporaryFile(block: suspend (File) -> R): R {
    var file: File? = null
    try {
        file = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            createTempFile("foo", "temp")
        }
        return block(file)
    } finally {
        file?.delete()
    }
}
t

trathschlag

02/21/2019, 8:31 PM
@uli if you just want to keep the original context, you could also use the magic
coroutineContext
extension property:
Copy code
suspend fun <R> useTemporaryFile(body: suspend (File) -> R): R {
    val outerContext = coroutineContext
    return withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
        val file = createTempFile("foo", "temp")

        try {
            withContext(outerContext) {
                body(file)
            }
        } finally {
            file.delete()
        }
    }
}
but I'm still not sure if there are any pitfalls regarding this. and for the general problem: what about
Copy code
suspend fun processImage() {
    val outerContext = coroutineContext

    withContext(NonCancellable) {
        val file = createFile()

        try {
            withContext(outerContext) {
                // do something with file
            }
        } finally {
            file.delete()
        }
    }
}
still not the most intuitive
e

elizarov

02/22/2019, 2:00 PM
@uli This behavior of
withContext
is designed on purpose. In a UI app, when we return back to the main thread we want to be sure that the UI is not destroyed (job is not cancelled) before we attempt to manipulate it.
We used to have an optional parameter to
withContext
control this behavior, but have dropped it, since it is too advanced and hard to use anyway.
So, indeed, you cannot easily return a resource using
withContext
. The is very similar to the following problem:
Copy code
fun createFile(): File =
  File.createTemporaryFile("foo", "temp")
  .also { doSomethingElse() } /// ooops... what if it fails? 
}
a

ansman

02/22/2019, 2:05 PM
The app could also crash so I still have to clean up old files so not a huge issue, but could be a problem with other closable resources such as sockets and input streams. It could be great if there an option to have a block that would be called when the Coroutine would be resumed but is canceled
e

elizarov

02/22/2019, 2:06 PM
We used to have it. We can have a separate version of
withContext
(maybe) with this kind of behavior.
I am at loos at how to properly name it, though
a

ansman

02/22/2019, 2:10 PM
The problem is not specific to
withContext
though but rather all functions that actually suspend
e

elizarov

02/22/2019, 2:10 PM
The problem is not specific to functions that suspend, but to all functions that can throw exception.
No all functions that suspend can throw an exception. The ones that can, are explicitly marked in their docs as being cancellable. Most of them do, though.
a

ansman

02/22/2019, 2:12 PM
Fair enough, but it’s a problem with
suspendCoroutine
for example
At least with that one you can check if it was canceled
e

elizarov

02/22/2019, 2:12 PM
this kind of pattern:
Copy code
val r = acquireResource()
try { ... } finally { r.close }
is inherently tricky. You have to make there is nothing between
acquireResource
and
try
and the implementation of
acquireResource
need to have extreme care for exceptional cases.
a

ansman

02/22/2019, 2:14 PM
Perhaps the solution is to have a try-finally pattern with Coroutines
When used it would guarantee that return values aren’t dropped
e

elizarov

02/22/2019, 2:15 PM
Yes. We definitely need a better try/finally for coroutines. We have a similar problem with `launch`:
Copy code
val r = acquireResource() 
launch { 
    try { ... } finally { r.close() }
}
It will not work, because launch may not even start.
So we need some nice solution that works both for
launch
and for
withContext
a

ansman

02/22/2019, 2:16 PM
The launch example would when using ATOMIC though?
e

elizarov

02/22/2019, 2:17 PM
Yes. Not a nice solution, but works. We used to have
CoroutineStart
paramater for
withContext
, too, but dropped it.
We need a better solution for both cases
4 Views