代码之家  ›  专栏  ›  技术社区  ›  Dean Hiller

在kotlin协同例程中,如何将数据范围界定为“请求路径”(例如MDC)?

  •  1
  • Dean Hiller  · 技术社区  · 1 年前

    MDC对日志记录来说是一件了不起的事情,我们仍然在java中使用异步编程的服务器端。上下文示例

    • 设置请求id MDC.put(“requestId”,UUID.generate())的筛选器;
    • 控制器已调用
    • 调用服务方法
    • 调用remotemethod(返回CompletableFuture)
    • 调用remotemethod返回后要调用的函数(很可能在另一个线程上)
    • 调用更多方法

    无论在世界上的哪个地方,开发人员都会在 webpieces project 或者Twitter的finatra项目,因为logback.xml(或者log4j 1或2)具有模式%X{requestId},所以会记录requestId。这使得开发人员永远不会忘记添加请求ID。例如,log.info(“一些消息”)显示在日志中,如下所示

    2023-07-21 12:43:49.681 INFO [myRequestIdHere] org.webpieces.asyncserver.impl.AsyncServerImpl
    some message
    

    注意,他们只是记录了“一些消息”,myRequestIdHere神奇地出现了。实际上,我们将其用于userId、customerId、requestId和一些部分。

    在老式的同步编程中,ThreadLocal被用来中断异步。Twitter有自己的Future和webpieces子类CompletableFuture(他们忘记了通过这个上下文) created XFuture 其通过该上下文。

    在使用协同例程时,如何在顶部进行筛选(或者在android中,我们正在做一个平台点击监听器,包装任何添加监听器的开发人员,这样每次点击都会得到一个请求id,我们会弹出一个错误,说遇到了错误,requestId:xxxx,每个日志magicaly都有该请求上下文的requestId)。

    这需要通过挂起函数和常规非挂起函数来工作 作为开发人员,可以从任何地方调用log.info()

    我们的一位开发人员指出 compositionlocal 由于协程上下文似乎只存在于挂起函数的上下文中:(

    或者,是否有某种方法可以从NOT挂起函数中获取上下文。毕竟,他们必须将其存储在某个ThreadLocal中,以便将其向下传递到下一个协同例程,此时他们可以将状态与稍后响应时运行的下一个协例程一起复制。(我知道他们必须复制,就好像他们不复制一样,你可以破坏这种状态,因为协同程序本质上只是以可读的方式编写承诺/未来逻辑的一种绝妙方式)。

    谢谢 院长

    协程上下文,但它不起作用,所以仍在研究中。

    1 回复  |  直到 1 年前
        1
  •  3
  •   broot    1 年前

    你说的大部分都是正确的。我们不能轻易使用 ThreadLocal 使用协程,因为它们经常切换线程。线程局部程序的标准替换是将数据存储在伴随协同程序传递的协同程序上下文中。然而,正如您所指出的,如果不在协同程序中运行,我们就无法访问上下文。

    不,没有办法访问suspend函数之外的协同程序上下文。上下文未存储在 ThreadLocal -它直接作为参数传递给每个suspend函数。每个suspend函数都有一个“隐藏”的延续参数,这就是上下文的存储位置。非挂起函数不会接收到延续,因此无法从它们访问上下文。

    我认为从挂起和非挂起上下文访问数据的最简单方法可能是使用 ThreadLocal ,但是为协程创建一个钩子,以便在恢复时恢复此线程本地值。事实上,协同程序已经提供了这样一个钩子。我们可以为给定的添加一个钩子 ThreadLocal 通过使用 asContextElement() 分机:

    private val variable = ThreadLocal<String>()
    
    suspend fun main(): Unit = withContext(Dispatchers.Default) {
        launch(variable.asContextElement("foo")) {
            repeat(5) {
                launch {
                    println("foo: ${Thread.currentThread().name}: ${variable.get()}")
                }
            }
        }
        launch(variable.asContextElement("bar")) {
            repeat(5) {
                launch {
                    println("bar: ${Thread.currentThread().name}: ${variable.get()}")
                }
            }
        }
    }
    

    样本结果:

    bar: DefaultDispatcher-worker-1: bar
    foo: DefaultDispatcher-worker-3: foo
    foo: DefaultDispatcher-worker-2: foo
    bar: DefaultDispatcher-worker-1: bar
    foo: DefaultDispatcher-worker-2: foo
    bar: DefaultDispatcher-worker-1: bar
    foo: DefaultDispatcher-worker-2: foo
    bar: DefaultDispatcher-worker-4: bar
    foo: DefaultDispatcher-worker-1: foo
    bar: DefaultDispatcher-worker-3: bar
    

    在本例中,我们运行两个并发的“请求”: foo bar ,对于 variable 。两个请求共享相同的线程,但的值 变量 总是与请求正确匹配。

    如果将其专门用于MDC和slf4j,请注意协同程序已经为其提供了支持: MDCContext 。我相信它在下面使用了与上面类似的技术。

    使用作用域启动新的协同程序时请小心,例如。 lifecycleScope.launch {} .通过显式提供作用域启动的协同程序不被视为当前协同程序的子程序,它们是独立的执行流,因此,它们不会正确传递您的存储值。您必须重新初始化挂钩,例如:

    lifecycleScope.launch(variable.asContextElement()) { ... }
    
        2
  •  0
  •   David Ibrahim    1 年前

    完成 Broot's answer ,要解决使用作用域启动新协同程序的问题,我们可以简单地创建一个新的扩展方法,将threadLocal添加到新作业的上下文中,如下所示:

    fun CoroutineScope.launchModified(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        return launch(this.coroutineContext + threadLocal.asContextElement() , start = start, block)
    }