记录一次 context 在异步任务中报 “context canceled” 的错误

2,194 阅读3分钟

记录一次context在异步任务中报“context canceled”的错误

背景

公司有一个产品是做数据库管理平台的,这个平台有一个接口接口,是在平台添加一个中间件代理,公司目前采用的方案是使用一个异步任务调用http封装的底层的组件去安装,同时使用结构体保存每个步骤的状态和接口返回的数据,然后使用短轮询去每秒查询一次任务的状态并反馈,这样前端就可以做一个进度条来展示。

问题

当一个请求过来之后,我从request中获取到了context,同时将这个context往下传,伪代码如下

//controller层
func Add (c echo.Context) {
  ctx := c.Reuqest().Context()
  //...
  sc := service.Addxxx(ctx)  //service层,负责具体业务,sc就是这个Steps的结构体,里面存储要执行的任务
  
  res := sc.Start()
  return c.JSON(http.StatusOK,res)
}
//service层
func Addxxx() {
  sc.addSteps(
    func () {
      //这里是具体的任务
    }
  )
}

当我满心欢喜代码写完之后,我在使用工具测试接口时,却返回context canceled的错误

排查过程

  • 从报错返回的信息得知,是一个context执行了Cancel的方法,导致后面的context都执行了Cancel方法,但是代码依然不知道,还在使用context,所以会报错。
  • 经过排查,在controller层里的方法直接走到了return语句,也就是一个请求结束,导致echo框架直接调用了Cancel方法,这时候这个ctx的子context还在异步调用封装的http方法去执行任务呢,所以导致调用http方法的时候报错context canceled

解决办法

一般的解决办法也就是换个新的context就行了,但是存在一个问题,那就是新的context没有原来context中的value,withTimeout等功能,但是目前有没有很好的办法只剥离context中的cancel方法,导致这就陷入了一个两难的局面。

最后经过讨论,只能暂时使用新的context,但是规定只有在使用异步的任务中使用的方式。来暂时解决这个问题,其实要是context有个去除cancel的方法、或者可以高度自定义context中cancel函数的方法就好了,这样可以截断cancel的传递,并且可以修改cancel函数。

实际问题

我之所以遇到这个问题的工作背景就是我们公司在做链路追踪中遇到的问题。众所周知,在链路追踪中有关于tracespan的概念,大概就是一个trance中有个一traceID和多个span,每个span也有一个spanID,这样在UI展示界面就可以按照如下图这样展示他们的调用关系

image-20230424132627234

而我在解决上面的问题替换了新的context后,发现原来的context中包含了tracdIDspanID这样的信息,由于这些信息没有传递到下层的context,导致新的context产生的span,乃至trace都没办法和原来的trace想关联,从而失去了链路的追踪效果。

解决办法

其实解决办法也是比较简单,就是在生成新的context的时候,将原来的traceIDSpanID复制到新的context上,这样就可以将新context生成的Span继续关联到原来的trace上了,一般复制的方法再相应的框架中就有了,需要仔细阅读以下,比如笔者使用的是jaeger框架进行UI展示,然后使用go.opentelemetry.io/otel/trace这个库里面的SpanContextFromContextContextWithSpanContext等方法。完整的代码如下

func NewContextWithSpanContext(parent context.Context) context.Context {
  spanContext := trace.SpanContextFromContext(parent)
  return trace.ContextWithSpanContext(context.Background(), spanContext)
}