Access ApplicationCall in object without propagation

Viewed 662

Is there a thread-safe method in Ktor where it is possible to statically access the current ApplicationCall? I am trying to get the following simple example to work;

object Main {

    fun start() {
        val server = embeddedServer(Jetty, 8081) {
            intercept(ApplicationCallPipeline.Call) {
                // START: this will be more dynamic in the future, we don't want to pass ApplicationCall
                Addon.processRequest() 
                // END: this will be more dynamic in the future, we don't want to pass ApplicationCall

                call.respondText(output, ContentType.Text.Html, HttpStatusCode.OK)
                return@intercept finish()
            }
        }
        server.start(wait = true)
    }
}

fun main(args: Array<String>) {
    Main.start();
}

object Addon {

    fun processRequest() {
        val call = RequestUtils.getCurrentApplicationCall()
        // processing of call.request.queryParameters
        // ...
    }
}

object RequestUtils {

    fun getCurrentApplicationCall(): ApplicationCall {
        // Here is where I am getting lost..
        return null
    }
}

I would like to be able to get the ApplicationCall for the current context to be available statically from the RequestUtils so that I can access information about the request anywhere. This of course needs to scale to be able to handle multiple requests at the same time.

I have done some experiments with dependency inject and ThreadLocal, but to no success.

2 Answers

Well, the application call is passed to a coroutine, so it's really dangerous to try and get it "statically", because all requests are treated in a concurrent context.

Kotlin official documentation talks about Thread-local in the context of coroutine executions. It uses the concept of CoroutineContext to restore Thread-Local values in specific/custom coroutine context.

However, if you are able to design a fully asynchronous API, you will be able to bypass thread-locals by directly creating a custom CoroutineContext, embedding the request call.

EDIT: I've updated my example code to test 2 flavors:

  • async endpoint: Solution fully based on Coroutine contexts and suspend functions
  • blocking endpoint: Uses a thread-local to store application call, as referred in kotlin doc.
import io.ktor.server.engine.embeddedServer
import io.ktor.server.jetty.Jetty
import io.ktor.application.*
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.launch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext

/**
 * Thread local in which you'll inject application call.
 */
private val localCall : ThreadLocal<ApplicationCall> = ThreadLocal();

object Main {

    fun start() {
        val server = embeddedServer(Jetty, 8081) {
            routing {
                // Solution requiring full coroutine/ supendable execution.
                get("/async") {
                    // Ktor will launch this block of code in a coroutine, so you can create a subroutine with
                    // an overloaded context providing needed information.
                    launch(coroutineContext + ApplicationCallContext(call)) {
                        PrintQuery.processAsync()
                    }
                }

                // Solution based on Thread-Local, not requiring suspending functions
                get("/blocking") {
                    launch (coroutineContext + localCall.asContextElement(value = call)) {
                        PrintQuery.processBlocking()
                    }
                }
            }

            intercept(ApplicationCallPipeline.ApplicationPhase.Call) {
                call.respondText("Hé ho", ContentType.Text.Plain, HttpStatusCode.OK)
            }
        }
        server.start(wait = true)
    }
}

fun main() {
    Main.start();
}

interface AsyncAddon {
    /**
     * Asynchronicity propagates in order to properly access coroutine execution information
     */
    suspend fun processAsync();
}

interface BlockingAddon {
    fun processBlocking();
}

object PrintQuery : AsyncAddon, BlockingAddon {
    override suspend fun processAsync() = processRequest("async", fetchCurrentCallFromCoroutineContext())

    override fun processBlocking() = processRequest("blocking", fetchCurrentCallFromThreadLocal())

    private fun processRequest(prefix : String, call : ApplicationCall?) {
        println("$prefix -> Query parameter: ${call?.parameters?.get("q") ?: "NONE"}")
    }
}

/**
 * Custom coroutine context allow to provide information about request execution.
 */
private class ApplicationCallContext(val call : ApplicationCall) : AbstractCoroutineContextElement(Key) {
    companion object Key : CoroutineContext.Key<ApplicationCallContext>
}

/**
 * This is your RequestUtils rewritten as a first-order function. It defines as asynchronous.
 * If not, you won't be able to access coroutineContext.
 */
suspend fun fetchCurrentCallFromCoroutineContext(): ApplicationCall? {
    // Here is where I am getting lost..
    return coroutineContext.get(ApplicationCallContext.Key)?.call
}

fun fetchCurrentCallFromThreadLocal() : ApplicationCall? {
    return localCall.get()
}

You can test it in your navigator:

http://localhost:8081/blocking?q=test1

http://localhost:8081/blocking?q=test2

http://localhost:8081/async?q=test3

server log output:

blocking -> Query parameter: test1
blocking -> Query parameter: test2
async -> Query parameter: test3

The key mechanism you want to use for this is the CoroutineContext. This is the place that you can set key value pairs to be used in any child coroutine or suspending function call.

I will try to lay out an example.

First, let us define a CoroutineContextElement that will let us add an ApplicationCall to the CoroutineContext.

class ApplicationCallElement(var call: ApplicationCall?) : AbstractCoroutineContextElement(ApplicationCallElement) {
    companion object Key : CoroutineContext.Key<ApplicationCallElement>
}

Now we can define some helpers that will add the ApplicationCall on one of our routes. (This could be done as some sort of Ktor plugin that listens to the pipeline, but I don't want to add to much noise here).

suspend fun PipelineContext<Unit, ApplicationCall>.withCall(
    bodyOfCall: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit
) {
    val pipeline = this
    val appCallContext = buildAppCallContext(this.call)
    withContext(appCallContext) {
        pipeline.bodyOfCall()
    }
}

internal suspend fun buildAppCallContext(call: ApplicationCall): CoroutineContext {
    var context = coroutineContext
    val callElement = ApplicationCallElement(call)
    context = context.plus(callElement)
    return context
}

And then we can use it all together like in this test case below where we are able to get the call from a nested suspending function:

suspend fun getSomethingFromCall(): String {
    val call = coroutineContext[ApplicationCallElement.Key]?.call ?: throw Exception("Element not set")
    return call.parameters["key"] ?: throw Exception("Parameter not set")
}


fun Application.myApp() {

    routing {
        route("/foo") {
            get {
                withCall {
                    call.respondText(getSomethingFromCall())
                }
            }
        }
    }
}

class ApplicationCallTest {

    @Test
    fun `we can get the application call in a nested function`() {
        withTestApplication({ myApp() }) {
            with(handleRequest(HttpMethod.Get, "/foo?key=bar")) {
                assertEquals(HttpStatusCode.OK, response.status())
                assertEquals("bar", response.content)
            }
        }
    }

}
Related