Using Kotlin coroutines in Spring MVC

[2022-04-03]

Short example how to use Kotlin coroutines to offload Spring MVC worker-threads

Recently I needed to add a REST endpoint in a Spring MVC app. To produce a result the endpoint had to call:

  • 3 databases,
  • a REST call,
  • KAFKA.

The queries to the databases could be particulary time consuming.

I wanted to offload the Spring MVC worker-threads and do as much IO operations in parallel as possible. Since Spring MVC was initially build around the “one request per thread” model I was not sure how this could be achieved (and somehow I haven’t noticed that Spring moved on years ago).

For running the individual IO operations Kotlin coroutines seemed like the natural fit. Kotlin coroutines can be a tricky subject, I learned a lot from the following 3 videos:

I also wanted to break off the computation after a specific period because of business reasons. Fortunately since version 3.2 Spring provides a convenience class DeferredResult which enables the decoupling of request processing from worker-threads. I wanted to tie a coroutine computation to the DeferredResult and hence the request itself.

The solution I came up with is the following:

class DeferredResultCoroutineScope<T>(timeout: Long):
    DeferredResult<T>(timeout), CoroutineScope {
    private var job: Job = Job() // parent Job of all coroutines that have started for this HTTP request
    override val coroutineContext: CoroutineContext
        get() = job
}

This creates a class which holds the computation and eventually the result itself (or an error).

As for the endpoint itself it would be just:

@PostMapping("/test")
@ResponseStatus(HttpStatus.ACCEPTED)
fun test(): DeferredResult<String> {
    return DeferredResultCoroutineScope<String>(2000L).
    apply {
        launch (Dispatchers.IO) {
            val c1 = async { call1() }
            val c2 = async { call2() }
            setResult(call3(c1.await(), c2.await()))
        }
        onTimeout {
            cancel()
            setErrorResult(AsyncRequestTimeoutException())
        }
    }
}

In order for call1, call2 and call3 to be “cancellable” they need to check if they have been cancelled themselves. Otherwise they will continue running even after the request times out.

This can be achieved by calling functions like ensureActive, isActive or any suspending function from kotlinx.coroutines (all support cancellation) in call1, call2, call3. Note that if you are calling blocking calls from the coroutine then they will not be cancelled byt the cancel() call!

I leave out error handling from the sample code so it’s more readable.