Emulating request scoped objects with Kotlin Coroutines

Background

A common pattern in web applications is to have data that is request scoped, that is, available during the lifetime of the current request only. An example of an object typically available in request scope is the current database session. The implementation of different request scoped objects however do not all work correctly when used in Kotlin coroutines.

The reason for that is they often use ThreadLocal variables. A ThreadLocal is a container object with the special behavior that the inner object it will return is unique to the current thread it is accessed on. The ThreadLocal is rarely visible directly to the user of a library or framework but is used behind the scenes, for example in the default CurrentSessionContext used by Hibernates SessionFactory.getCurrentSession() and the backing adapters of the MDC object in SLF4J.

A typical web framework will set the values of these objects at the start of a request and then clear them at the end of the same request. This works fine as long as all the users of these objects are accessing them on the same thread the request started on. The typical use case of Kotlin coroutines however will have them potentially suspend and then resume on any arbitrary thread in any number of different thread pools during the life-cycle of a single request.

We therefore need a different way to implement request scoped objects that preferably don’t require us to pass the objects as arguments through a long chain of function calls.

Enter coroutine contexts

The preferred way to store values that needs to be available for the duration of a Kotlin coroutine is to extend the current CoroutineContext to add more elements to it, each element is then accessible anywhere inside a suspending function by key lookup.

An example coroutine context

Here is an example context that simply stores a single requestId value.

class RequestContext(
        val requestId: String
) : AbstractCoroutineContextElement(RequestContext) {
    companion object Key : CoroutineContext.Key
}

Notice the RequestContext.Key companion object, it is what will enable us to look up the RequestContext object later inside of our coroutine.

Using the new context

In order to use our context we need to pass it as an argument to the coroutine launcher. Then, inside the coroutine, we use the global coroutineContext property to look up our context element.

fun test() {
    val context = RequestContext("my-request-id")
    runBlocking(context) {
        val requestId = coroutineContext[RequestContext]?.requestId
        println(requestId) // prints "my-request-id"
    }
}

Easy enough, but what happens if we launch a new coroutine from within the current one? Perhaps we use async to wrap a blocking call to an external service or a database. Will our context element be available within this new async call? The answer, unfortunately, is no.

fun test() {
    val context = RequestContext("my-request-id")
    runBlocking(context) {
        nestedCoroutine()
    }
}

suspend fun nestedCoroutine() {
    return async {
        // imagine a blocking call here
        val requestId = coroutineContext[RequestContext]?.requestId
        println(requestId) // prints "null"
    }.await()
}

The reason that our element is no longer available is that async by default launches with a completely separate context: DefaultDispatcher (the CommonPool).

In order to preserve our current context when launching a new coroutine we will need to explicitly pass it to the launcher, like we did with the original coroutine.

suspend fun nestedCoroutine() {
    return async(coroutineContext) {
        // imagine a blocking call here
        val requestId = coroutineContext[RequestContext]?.requestId
        println(requestId) // prints "my-request-id"
    }.await()
}

Combining coroutine contexts

But what if we are already using a separate context when launching our coroutine? Maybe we have an IOPool context that executes the coroutine on a thread pool meant for IO workloads, we don’t want to replace that behavior when using our new RequestContext.

The solution is to compose the two contexts, this is easily done using the plus operator defined for contexts:

suspend fun nestedCoroutine() {
    val combinedContext = coroutineContext + IOPool
    return async(combinedContext) {
        // imagine a blocking call here
        val requestId = coroutineContext[RequestContext]?.requestId
        println(requestId) // prints "my-request-id"
    }.await()
}

The plus operator associativeness is such that the behavior and elements of the right-hand operand will override the behavior and elements of the left-hand-operand. In this case we will get a context that includes both the dispatching behavior of the IOPool and the RequestContext-element in the current coroutineContext.

The need to remember to reuse the current context when launching new coroutines is a bit annoying but can be alleviated by defining custom coroutine launchers in our applications.

suspend fun <T> asyncIO(block: suspend () -> T): Deferred<T> {
    val combinedContext = coroutineContext + IOPool
    return async(combinedContext) {
         block()
    }
}

Read more

One thought on “Emulating request scoped objects with Kotlin Coroutines

  1. For anyone reading this story it is important to add that the problem of context inheritance had existed only in pre-release versions of kotlinx.coroutines library. Before making a stable 1.0 release we had introduced the concept of “Structured Concurrency” makes inheritance of the coroutine context a default behavior and solves a host of other problems. You can read more about this change here: https://medium.com/@elizarov/structured-concurrency-722d765aa952

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s