【BUG】记一次context canceled的报错

文章目录

    • 案例分析
    • gorm源码解读
    • gin context 生命周期
      • context什么时候cancel的
      • 什么时候context会被动cancel掉呢?
    • 野生协程如何处理

案例分析

报错信息

{
    "L":"ERROR",
    "T":"2024-12-17T11:11:33.005+0800",
    "file":"*/log.go:61",
    "message":"sql_trace",
    "__type":"sql",
    "trace_id":"6ab69b5d333de40c8327d8572336fa2c",
    "error":"context canceled; invalid connection",
    "elapsed":"2.292ms",
    "rows":0,
    "sql":"UPDATE `logs` SET `response_time`=1734405092,`status`='success' WHERE id = 226081"
}

案发代码:

func Sync(c *gin.Context) {
	var params services.Params
	// 参数绑定
	c.ShouldBindBodyWith(&params, binding.JSON)
	// 参数效验
	
	// 记录日志
	...
	
	// 开协程 更新日志
	go func() {
			defer helpers.Recovery(c)
			models.Log{Ctx: c.Request.Context()}.UpdateLog(logId, res)
		}()
	c.JSON(200, response.Success(nil))
	return 
}



func UpdateLog(id uint, r *services.ResJson) bool {
	exec := models.DefaultDB().WithContext(s.Ctx).Where("id  = ?", id).Model(&Log{}).Updates(map[string]interface{}{
			"status":        StatusSuccess,
			"response_time": time.Now().Unix(),
		})
	return exec.RowsAffected > 0
}

在更新数据库时,开了一个协程去更新

gorm源码解读

gorm Find、Update方法会触发GORM内部的处理器链,其中包括构建SQL语句、准备参数等。

最终,会调用到processor.Execute(db *DB)方法,这个方法会遍历并执行一系列注册的回调函数。

gorm.io/gorm@v1.25.11/finisher_api.go

// Update updates column with value using callbacks. Reference: https://gorm.io/docs/update.html#Update-Changed-Fields
func (db *DB) Update(column string, value interface{}) (tx *DB) {
	tx = db.getInstance()
	tx.Statement.Dest = map[string]interface{}{column: value}
	return tx.callbacks.Update().Execute(tx)
}


// gorm.io/gorm@v1.25.11/callbacks.go

func (p *processor) Execute(db *DB) *DB {
	...
	for _, f := range p.fns {
		f(db)
	}
}

// 注册回调函数
gorm@v1.25.11/callbacks/callbacks.go

func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
	enableTransaction := func(db *gorm.DB) bool {
		return !db.SkipDefaultTransaction
	}

	if len(config.CreateClauses) == 0 {
		config.CreateClauses = createClauses
	}
	if len(config.QueryClauses) == 0 {
		config.QueryClauses = queryClauses
	}
	if len(config.DeleteClauses) == 0 {
		config.DeleteClauses = deleteClauses
	}
	if len(config.UpdateClauses) == 0 {
		config.UpdateClauses = updateClauses
	}

	createCallback := db.Callback().Create()
	createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	createCallback.Register("gorm:before_create", BeforeCreate)
	createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
	createCallback.Register("gorm:create", Create(config))
	createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
	createCallback.Register("gorm:after_create", AfterCreate)
	createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	createCallback.Clauses = config.CreateClauses

	queryCallback := db.Callback().Query()
	queryCallback.Register("gorm:query", Query)
	queryCallback.Register("gorm:preload", Preload)
	queryCallback.Register("gorm:after_query", AfterQuery)
	queryCallback.Clauses = config.QueryClauses

	deleteCallback := db.Callback().Delete()
	deleteCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	deleteCallback.Register("gorm:before_delete", BeforeDelete)
	deleteCallback.Register("gorm:delete_before_associations", DeleteBeforeAssociations)
	deleteCallback.Register("gorm:delete", Delete(config))
	deleteCallback.Register("gorm:after_delete", AfterDelete)
	deleteCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	deleteCallback.Clauses = config.DeleteClauses

	updateCallback := db.Callback().Update()
	updateCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	updateCallback.Register("gorm:setup_reflect_value", SetupUpdateReflectValue)
	updateCallback.Register("gorm:before_update", BeforeUpdate)
	updateCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(false))
	updateCallback.Register("gorm:update", Update(config))
	updateCallback.Register("gorm:save_after_associations", SaveAfterAssociations(false))
	updateCallback.Register("gorm:after_update", AfterUpdate)
	....
}

gorm.io/gorm@v1.25.11/callbacks/update.go

// Update update hook
func Update(config *Config) func(db *gorm.DB) {
	supportReturning := utils.Contains(config.UpdateClauses, "RETURNING")

	return func(db *gorm.DB) {
		if db.Error != nil {
			return
		}

		if db.Statement.Schema != nil {
			for _, c := range db.Statement.Schema.UpdateClauses {
				db.Statement.AddClause(c)
			}
		}

		if db.Statement.SQL.Len() == 0 {
			db.Statement.SQL.Grow(180)
			db.Statement.AddClauseIfNotExists(clause.Update{})
			if _, ok := db.Statement.Clauses["SET"]; !ok {
				if set := ConvertToAssignments(db.Statement); len(set) != 0 {
					defer delete(db.Statement.Clauses, "SET")
					db.Statement.AddClause(set)
				} else {
					return
				}
			}

			db.Statement.Build(db.Statement.BuildClauses...)
		}

		checkMissingWhereConditions(db)

		if !db.DryRun && db.Error == nil {
			if ok, mode := hasReturning(db, supportReturning); ok {
				// Update函数最终会调用到底层数据库驱动的QueryContext方法,这个方法接受一个context.Context对象作为参数。
				if rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...); db.AddError(err) == nil {
					dest := db.Statement.Dest
					db.Statement.Dest = db.Statement.ReflectValue.Addr().Interface()
					gorm.Scan(rows, db, mode)
					db.Statement.Dest = dest
					db.AddError(rows.Close())
				}
			} else {
				result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)

				if db.AddError(err) == nil {
					db.RowsAffected, _ = result.RowsAffected()
				}
			}
		}
	}
}

调用数据库驱动:

Update函数最终会调用到底层数据库驱动的QueryContext方法,这个方法接受一个context.Context对象作为参数。

go1.22.3/src/database/sql/sql.go:1727

// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
	var rows *Rows
	var err error

	err = db.retry(func(strategy connReuseStrategy) error {
		rows, err = db.query(ctx, query, args, strategy)
		return err
	})

	return rows, err
}

底层数据库连接:

QueryContext方法会进一步调用query方法,这个方法会处理数据库连接的重试逻辑。

在query方法中,会调用conn方法来获取一个数据库连接,并在这个连接上执行查询。

conn方法会处理context的取消和超时信号,如果context被取消或超时,它会中断数据库连接操作并返回错误。

go1.22.3/src/database/sql/sql.go:1748

func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) {
	dc, err := db.conn(ctx, strategy)
	if err != nil {
		return nil, err
	}

	return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	db.mu.Lock()
	if db.closed {
		db.mu.Unlock()
		return nil, errDBClosed
	}
	// Check if the context is expired.
	select {
	default:
	case <-ctx.Done():
		db.mu.Unlock()
		return nil, ctx.Err()
	}

那为什么会出现context canceled?

gin context 生命周期

在这里插入图片描述

大多数情况下,context一直能持续到请求结束
当请求发生错误的时候,context会立刻被cancel掉

context什么时候cancel的

server端接受新请求时会起一个协程go c.serve(connCtx)

func (srv *Server) Serve(l net.Listener) error {
 // ...
    for {
        rw, err := l.Accept()
        connCtx := ctx
     // ...
        go c.serve(connCtx)
    }
}

协程里面for循环从链接中读取请求,重点是这里每次读取到请求的时候都会启动后台协程(w.conn.r.startBackgroundRead())继续从链接中读取。

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
                // ...
   // HTTP/1.x from here on.

   ctx, cancelCtx := context.WithCancel(ctx)
   c.cancelCtx = cancelCtx
   defer cancelCtx()

             // ...
   for {
      // 从链接中读取请求
      w, err := c.readRequest(ctx)
      if c.r.remain != c.server.initialReadLimitSize() {
         // If we read any bytes off the wire, we're active.
         c.setState(c.rwc, StateActive, runHooks)
      }

                            // ....

      // 启动协程后台读取链接
      if requestBodyRemains(req.Body) {
         registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
      } else {
         w.conn.r.startBackgroundRead()
      }

      // ...
      // 这里转到gin里面的serverHttp方法
      serverHandler{c.server}.ServeHTTP(w, w.req)

      // 请求结束之后cancel掉context
      w.cancelCtx()
      // ...
   }
}

gin中执行ServeHttp方法

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // ...
    // 执行我们写的handle方法
          engine.handleHTTPRequest(c)
    // ...
}

正常请求结束之后gin框架会主动cancel掉context, ctx会清空,回收到ctx pool中。

// github.com/gin-gonic/gin@v1.7.7/gin.go

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}



// github.com/gin-gonic/gin@v1.7.7/context.go
func (c *Context) reset() {
	c.Writer = &c.writermem
	c.Params = c.Params[0:0]
	c.handlers = nil
	c.index = -1

	c.fullPath = ""
	c.Keys = nil
	c.Errors = c.Errors[0:0]
	c.Accepted = nil
	c.queryCache = nil
	c.formCache = nil
	*c.params = (*c.params)[:0]
	*c.skippedNodes = (*c.skippedNodes)[:0]
}

什么时候context会被动cancel掉呢?

秘密就在w.conn.r.startBackgroundRead()这个后台读取的协程里了。

func (cr *connReader) startBackgroundRead() {
    // ...
    go cr.backgroundRead()
}

func (cr *connReader) backgroundRead() {
    n, err := cr.conn.rwc.Read(cr.byteBuf[:])
 // ...
    if ne, ok := err.(net.Error); ok && cr.aborted && ne.Timeout() {
        // Ignore this error. It's the expected error from
        // another goroutine calling abortPendingRead.
    } else if err != nil {
                    cr.handleReadError(err)
    }
    // ...
}

func (cr *connReader) handleReadError(_ error) {
       // 这里cancel了context
    cr.conn.cancelCtx()
    cr.closeNotify()
}

startBackgroundRead -> backgroundRead -> handleReadError。在handleReadError函数里面会把context cancel掉。

当服务端在处理业务的同时,后台有个协程监控链接的状态,如果链接有问题就会把context cancel掉。(cancel的目的就是快速失败——业务不用处理了,就算服务端返回结果了,客户端也不处理了)

野生协程如何处理

  • http请求如有野生协程,不能使用request context(因为response之后context就会被cancel掉了),应当使用独立的context(比如context.Background()
  • 禁用野生协程,控制协程生命周期

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/938263.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

召回系统介绍

一、以Lucene为例介绍召回系统 1、倒排检索 Lucene的倒排索引由 Term Index -> TermDictionary -> Posting List 三层组成&#xff0c;倒排检索实际上就是通过分词Term查询到倒排拉链&#xff0c;然后对所有拉链进行合并。 Term-> Posting List&#xff0c;可以直接…

Ubuntu22.04系统下MVS运行海康威视工业相机

之前的开发环境是Ubuntu16.04&#xff0c;最近因项目需求换到了Ubuntu22.04系统&#xff0c;安装了ROS2-humble&#xff0c;重新记录下开发过程。 Ubuntu16.04系统可参考&#xff1a; VMware虚拟机中Ubuntu16.04系统下通过MVS运行海康威视工业相机 Linux环境中对海康威视工业相…

慧知开源充电桩平台 - OCPP充电桩协议越南充电平台:多语种支持、多元支付、本地化策略

越南充电新体验&#xff1a;多语种支持&#xff0c;便捷支付&#xff01; 助力充电桩运营本土化落地&#xff0c;为越南市场提供定制化解决方案 随着全球电动汽车市场的迅猛发展&#xff0c;越南作为东南亚新兴的汽车市场&#xff0c;对电动汽车充电基础设施的需求也在急剧增…

基于Clinical BERT的医疗知识图谱自动化构建方法,双层对比框架

基于Clinical BERT的医疗知识图谱自动化构建方法&#xff0c;双层对比框架 论文大纲理解1. 确认目标2. 目标-手段分析3. 实现步骤4. 金手指分析 全流程核心模式核心模式提取压缩后的系统描述核心创新点 数据分析第一步&#xff1a;数据收集第二步&#xff1a;规律挖掘第三步&am…

华为ensp--BGP路径选择-Preferred Value

学习新思想&#xff0c;争做新青年。今天学习的是BGP路径选择-Preferred Value 实验目的 理解BGP路由信息首选值&#xff08;Preferred Value&#xff09;的作用 掌握修改Preferred Value属性的方法 掌握通过修改Preferred Value属性来实现流量分担的方法 实验拓扑 实验要求…

如何在OpenCV中运行自定义OCR模型

我们首先介绍如何获取自定义OCR模型&#xff0c;然后介绍如何转换自己的OCR模型以便能够被opencv_dnn模块正确运行&#xff0c;最后我们将提供一些预先训练的模型。 训练你自己的 OCR 模型 此存储库是训练您自己的 OCR 模型的良好起点。在存储库中&#xff0c;MJSynthSynthTe…

“从零到一:揭秘操作系统的奇妙世界”【操作系统的发展】

1.手工操作阶段 此时没有OS&#xff0c;用户采用人工操作方式进行。 方式&#xff1a;程序员在纸带机上打孔---计算机读取---结果输出到纸袋机上---程序员取走结果 缺点&#xff1a;耗时长&#xff0c;难度大、用户独占全机、人机速度矛盾导致资源利用率低 2.单批道处理系统 引…

二叉树理论基础篇

这里写目录标题 二叉树的种类**满二叉树&#xff08;Full Binary Tree&#xff09;****完全二叉树&#xff08;Complete Binary Tree&#xff09;****二叉搜索树&#xff08;Binary Search Tree&#xff0c;BST&#xff09;**平衡二叉搜索树 二叉树的存储方式二叉树的遍历方式二…

【376.2协议】国网_用电信息采集系统通信协议

【376.2协议】用电信息采集系统通信协议 文章目录 【376.2协议】用电信息采集系统通信协议1、帧格式2、各传输帧2.1 控制域 C (一个字节|8个位)2.2 用户数据区格式2.2.1 信息域2.2.2 地址域2.2.3 应用数据域 3、式例 1、帧格式 帧格式定义规则起始字符固定报文头&#xff08; …

鸿蒙项目云捐助第十一讲鸿蒙App应用的捐助成功自定义对话框组件实现

在生活中&#xff0c;用户做了一个好事后&#xff0c;很多场合都会收到一份感谢。在捐助的行业也是一样的&#xff0c;用户捐出了一片爱心&#xff0c;就会收获一份温情。这里的温情是通过自定义对话框实现的。 一、通过自定义对话框组件实现捐款成功的信息页 这里用户捐款成…

leetcode区间部分笔记

区间部分 1. 汇总区间2. 合并区间3. 插入区间4. 用最少数量的箭引爆气球 1. 汇总区间 给定一个 无重复元素 的 有序 整数数组 nums 。 返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说&#xff0c;nums 的每个元素都恰好被某个区间范围所覆盖&#xff0c;并…

spring学习(spring-bean实例化(静态工厂))

目录 一、spring容器实例化bean的几种方式。 二、spring容器使用静态工厂方式实现bean实例化。 &#xff08;1&#xff09;基本介绍。 1、静态工厂&#xff1f; 2、"factory-method"属性。 3、二种操作方式。 方法一。 方法二。 &#xff08;2&#xff09;demo(案例)…

25年宁德时代社招在职晋升Verify测评SHL题库:语言理解+数字推理考什么?

宁德时代的社招测评采用Verify系统&#xff0c;主要分为两大核心部分&#xff1a;语言理解和数字推理。 1. **语言理解部分**&#xff1a;包括阅读理解、逻辑填空和语句排序等题型。要求应聘者在17分钟内完成30题&#xff0c;旨在考察应聘者的阅读速度、理解准确性和逻辑性。 …

2024数证杯初赛

计算机取证 请根据计算机检材&#xff0c;回答以下问题&#xff1a;(32个小题&#xff0c;共76分 1.[填空题对计算机镜像进行分析&#xff0c;计算该镜像中ESP分区的SM3值后8位为&#xff1f;&#xff08;答案格式&#xff1a;大写字母与数字组合&#xff0c;如&#xff1a;D…

典型案例 | 旧PC新蜕变!东北师范大学依托麒麟信安云“旧物焕新生”

东北师范大学始建于1946年&#xff0c;坐落于吉林省长春市&#xff0c;是中国共产党在东北地区创建的第一所综合性大学。作为国家“双一流”建设高校&#xff0c;学校高度重视教学改革和科技创新&#xff0c;校园信息化建设工作始终走在前列。基于麒麟信安云&#xff0c;东北师…

项目二十三:电阻测量(需要简单的外围检测电路,将电阻转换为电压)测量100,1k,4.7k,10k,20k的电阻阻值,由数码管显示。要求测试误差 <10%

资料查找&#xff1a; 01 方案选择 使用单片机测量电阻有多种方法&#xff0c;以下是一些常见的方法及其原理&#xff1a; 串联分压法&#xff08;ADC&#xff09; 原理&#xff1a;根据串联电路的分压原理&#xff0c;通过测量已知电阻和待测电阻上的电压&#xff0c;计算出…

ST-Linker V2 烧录器详解说明文档

目录 ST-Linker v2烧录器介绍 STM8烧录口 STM32烧录接口 JTAG烧录接口 ​​​​​​​ ​​​​​​​ ​​​​​​​ 编写不易&#xff0c;仅供学习&#xff0c;请勿搬运&#xff0c;感谢理解 ST-Linker v2烧录器介绍 图片中是两种IC芯片的烧录器&#x…

在Centos7上安装MySQL数据库 How to install MySQL on Centos 7

执行以下命令&#xff0c;下载并安装MySQL。 wget http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm && yum -y install mysql57-community-release-el7-10.noarch.rpm && yum install -y mysql-community-server --nogpgcheck执行以下…

FireFox火狐浏览器企业策略禁止更新

一直在用火狐浏览器&#xff0c;但是经常提示更新&#xff0c;进入浏览器右上角就弹出提示&#xff0c;比较烦。多方寻找&#xff0c;一直没有找到合适的方案&#xff0c;毕竟官方没有给出禁用检查更新的选项&#xff0c;甚至about:config里都没有。 最终找到了通过企业策略控…

【Qt】按钮类控件:QPushButton、QRadioButton、QCheckBox、ToolButton

目录 QPushButton 例子&#xff1a; QRadioButton 例子&#xff1a; 按钮的常见信号函数 单选按钮分组 例子&#xff1a; QCheckButton 例子&#xff1a; QToolButton QWidget的常见属性及其功能对于它的派生类控件都是有效的(也就是Qt中的各种控件)&#xff0c;包括…