启动协程前先看ctx的生命周期,小心莫名奇妙吃一BUG

0 阅读1分钟

下面这段代码,你能看出什么问题吗?

伪代码

package main

import (
    "context"
    "net/http"
)

func main() {
    // 模拟创建订单接口
    http.HandleFunc("/createOrder", func(w http.ResponseWriter, r *http.Request) {
       ctx := r.Context()
       // 创建订单
       orderDo := createOrder(ctx, reqParams)
       
       // 异步推送订单到供应链
       go SubmitToSupplier(ctx, orderDo)
       
       // 响应用户下单成功
       w.WriteHeader(http.StatusOK)
       w.Write([]byte(`{"code":"ok"}`))
    })
    http.ListenAndServe(":8080", nil)
}

问题 1. SubmitToSupplier 运行失败

  • 原因:客户端请求取消、超时,或服务端响应结束时,服务端的ctx会被cancel掉,此时SubmitToSupplier中支持ctx相关的操作就不会执行,如一些支持ctx的db、http请求等。同理,GRPC也有相同的问题,一些http框架可自行验证,以防吃BUG。
  • 解决:封闭CopyContext方法来复制ctx(保留Value,重写Done方法不返回值,这样就永远不受外部取消了),除了用于异步ctx的传递外,一些不支持整体事务的方法也需要copy,防止数据不一致。

问题 2: 协程执行中断

  • 原因:未被封装启动的协程,服务重启时不知道有正在运行的任务,无法做到平滑关闭。
  • 解决:封装统一启动协程的方法,程序收到退出信号后,先检查是否有协程未处理完。还有个好处是可以纯一包装recover的处理,防止协程panic导致主进程退出。

最后看下 net/http包中cancel的操作流程:

image.png

image.png