Доступ к ApplicationCall в объекте без распространения

Есть ли в Ktor поточно-ориентированный метод, позволяющий получить статический доступ к текущему ApplicationCall? Я пытаюсь заставить работать следующий простой пример;

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
    }
}

Я хотел бы, чтобы ApplicationCall для текущего контекста был доступен статически из RequestUtils, чтобы я мог получить доступ к информации о запросе где угодно. Это, конечно, необходимо масштабировать, чтобы иметь возможность обрабатывать несколько запросов одновременно.

Я провел несколько экспериментов с внедрением зависимостей и ThreadLocal, но безуспешно.


person Thizzer    schedule 16.05.2020    source источник


Ответы (2)


Что ж, вызов приложения передается сопрограмме, поэтому действительно опасно пытаться получить его «статически», потому что все запросы обрабатываются в параллельном контексте.

В официальной документации Kotlin говорится о Thread-local в контексте выполнения сопрограмм. Он использует концепцию CoroutineContext для восстановления значений Thread-Local в конкретном / настраиваемом контексте сопрограммы.

Однако, если вы можете разработать полностью асинхронный API, вы сможете обойти локальные переменные потока, напрямую создав настраиваемый CoroutineContext, встраивая вызов запроса.

ИЗМЕНИТЬ: я обновил свой пример кода, чтобы протестировать 2 варианта:

  • Конечная точка async: решение, полностью основанное на контекстах Coroutine и функциях приостановки
  • блокирующая конечная точка: использует локальный поток для хранения вызова приложения, как указано в 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()
}

Вы можете протестировать это в своем навигаторе:

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

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

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

вывод журнала сервера:

blocking -> Query parameter: test1
blocking -> Query parameter: test2
async -> Query parameter: test3
person amanin    schedule 19.05.2020

Ключевым механизмом, который вы хотите использовать для этого, является CoroutineContext. Это место, где вы можете установить пары ключ-значение, которые будут использоваться в любой дочерней сопрограмме или в вызове функции приостановки.

Постараюсь на примере выложить.

Во-первых, давайте определим CoroutineContextElement, который позволит нам добавить ApplicationCall к CoroutineContext.

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

Теперь мы можем определить несколько помощников, которые добавят ApplicationCall на один из наших маршрутов. (Это можно было бы сделать как своего рода плагин Ktor, который слушает конвейер, но я не хочу добавлять здесь много шума).

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
}

И затем мы можем использовать все это вместе, как в этом тестовом примере ниже, где мы можем получить вызов от вложенной функции приостановки:

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)
            }
        }
    }

}
person Laurence    schedule 20.05.2020
comment
Ах, черт, я вижу, что мой ответ очень похож на @amanin. Ну что ж, оставим это здесь, так как, возможно, вспомогательные функции добавят некоторые идеи по реализации. - person Laurence; 20.05.2020