Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convey intended thread type (IO, default, main) when declaring suspend function

When designing an API with a suspend function, sometimes I want to convey that this function should be called on, say, an IO thread. Other times that it is essential to do so.

Often it seems obvious; for example a database call should be called using Dispatchers.IO but if it's an interface function, then the caller cannot assume this.

What is the best approach here?

like image 680
Mark Avatar asked Dec 17 '22 20:12

Mark


2 Answers

If the suspend function really must run in a specific context, then declare it directly in the function body.

suspend fun doInIO() = withContext(Dispatchers.IO) {

}

If the caller should be able to change the dispatcher, the function can add the dispatcher as a default parameter.

suspend fun doInIO(context: CoroutineContext = Dispatchers.IO) = withContext(context) {

}
like image 197
Rene Avatar answered Apr 30 '23 13:04

Rene


There is no strict mechanism for contracts like that, so you are flexible with choosing the mechanism that suits you and your team.

1) Always use withContext(Dispatcher.IO). This is both strict and performant, if a method is invoked from within IO context it will be fast-path'ed.

2) Naming/annotation-based conventions. You can make an agreement in the team that any method which ends with IO or has a specific annotation should be invoked with Dispatchers.IO. This approach works mostly in small teams and only for project-private API. Once you start exporting it as a library/module for other teams such contracts tend to be broken.

3) You can mix the previous approach with a validation:

suspend fun readFile(file: ...) {
    require(coroutineContext[ContinuationInterceptor] == Dispatcher.IO) {
      "Expected IO dispatcher, but has ${coroutineContext[ContinuationInterceptor]} instead"
    }
    // read file
}

But this validation works only if you are not wrapping IO dispatcher in some kind of delegate/proxy. In that case, you should make validation aware of such proxies, something like:

fun validateIoDispatcher(dispatcher: ContinuationInterceptor) {
    if (dispatcher is Dispatchers.IO) return
    if (dispatcher is ProjectSpecificIoDispatcher) return
    if (dispatcher is ProjectSpecificWrapperDispatcher) {
        validateIoDispatcher(dispatcher.delegate)
    } else {
        error("Expected IO dispatcher, but has $dispatcher")
    }
}

I want to convey that this function should be called on, say, an IO thread. Other times that it is essential to do so.

Not sure what the difference is between "should" and "essential", but having these approaches in mind you can combine it with default method parameters such as suspend fun probablyIO(dispatcher: CoroutineDispatcher = Dispatchers.IO) or more flexible naming/annotation conventions.

like image 35
qwwdfsad Avatar answered Apr 30 '23 13:04

qwwdfsad