【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(¶ms, 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.gofunc (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.CreateClausesqueryCallback := db.Callback().Query()queryCallback.Register("gorm:query", Query)queryCallback.Register("gorm:preload", Preload)queryCallback.Register("gorm:after_query", AfterQuery)queryCallback.Clauses = config.QueryClausesdeleteCallback := 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.DeleteClausesupdateCallback := 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.Destdb.Statement.Dest = db.Statement.ReflectValue.Addr().Interface()gorm.Scan(rows, db, mode)db.Statement.Dest = destdb.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 *Rowsvar err errorerr = 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 = cancelCtxdefer 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掉contextw.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 = reqc.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.writermemc.Params = c.Params[0:0]c.handlers = nilc.index = -1c.fullPath = ""c.Keys = nilc.Errors = c.Errors[0:0]c.Accepted = nilc.queryCache = nilc.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了contextcr.conn.cancelCtx()cr.closeNotify()
}
startBackgroundRead
-> backgroundRead
-> handleReadError
。在handleReadError函数里面会把context cancel掉。
当服务端在处理业务的同时,后台有个协程监控链接的状态,如果链接有问题就会把context cancel掉。(cancel的目的就是快速失败——业务不用处理了,就算服务端返回结果了,客户端也不处理了)
野生协程如何处理
- http请求如有野生协程,不能使用request context(因为response之后context就会被cancel掉了),应当使用独立的context(比如
context.Background()
) - 禁用野生协程,控制协程生命周期
相关文章:

【BUG】记一次context canceled的报错
文章目录 案例分析gorm源码解读gin context 生命周期context什么时候cancel的什么时候context会被动cancel掉呢? 野生协程如何处理 案例分析 报错信息 {"L":"ERROR","T":"2024-12-17T11:11:33.0050800","file"…...

[SWPUCTF 2022 新生赛]善哉善哉
右击查看属性 然后放在010查看一下 摩斯密码解码 用佛曰解码 用md5加密看一下 最后一步md5,没有说明编码,尝试utf8和gbk ss4 施主,此次前来,不知有何贵干? import hashlib print(hashlib.md5(ss4.encode(utf8)).hexdigest())f…...

《PCI密码卡技术规范》题目
单选1 在《PCI密码卡技术规范》中,下列哪项不属于PCI密码卡的功能()。 A.密码运算功能 B.密钥管理功能 C.物理随机数产生功能 D.随主计算机可信检测功能 正确答案:D. <font style"color:#DF2A3F;">解析&…...

城市大屏设计素材宝库:助力设计师高效创作
城市大屏设计工作要求设计师在有限的时间内打造出令人惊叹的视觉效果,而拥有一套必备的素材集无疑是如虎添翼。这些素材犹如设计师的得力助手,无论是构建整体布局的设计模板,还是点缀细节的图标图形,都能在关键时刻发挥重要作用&a…...

HCIA-Access V2.5_5_1PON系统概述_PON网络概述
PON网络设备有很多各类,可应用于不同的业务场景,从而实现不同的业务,本章介绍PON系统应用组成,分析PON系统的硬件结构和模块功能,描述PON系统的应用场景,帮助你对接入网中设备形态有更深刻的印象。 你可以…...

群落生态学研究进展】Hmsc包开展单物种和多物种分析的技术细节及Hmsc包的实际应用
联合物种分布模型(Joint Species Distribution Modelling,JSDM)在生态学领域,特别是群落生态学中发展最为迅速,它在分析和解读群落生态数据的革命性和独特视角使其受到广大国内外学者的关注。在学界不同研究团队研发出…...

一个开源的自托管虚拟浏览器项目,支持在安全、私密的环境中使用浏览器
大家好,今天给大家分享一个开源的自托管虚拟浏览器项目Neko,旨在利用 WebRTC 技术在 Docker 容器中运行虚拟浏览器,为用户提供安全、私密且多功能的浏览体验。 项目介绍 Neko利用 WebRTC 技术在 Docker 容器中运行虚拟浏览器,提供…...

职场上,如何做好自我保护?
今天我们讨论一个话题:在职场上,如何保护好自己?废话不多说,我们直接上干货。 (一) 1.时刻准备一点零食或代餐,如果遇到长时间的会议,就补充点能量。代餐最好选流体,这…...

华为数通最新题库 H12-821 HCIP稳定过人中
以下是成绩单和考试人员 HCIP H12-831 HCIP H12-725 安全中级...

mac iterm2 使用 lrzsz
前言 mac os 终端不支持使用 rz sz 上传下载文件,本文提供解决方法。 mac 上安装 brew install lrzsz两个脚本 注意:/usr/local/bin/iterm2-send-zmodem.sh 中的 sz命令路径要和你mac 上 sz 命令路径一致。 /usr/local/bin/iterm2-recv-zmodem.sh 中…...
PostgreSql-学习06-libpq之同步命令处理
目录 一、环境 二、介绍 三、函数 1、PQsetdbLogin (1)作用 (2)声明 (3)参数介绍 (4)检测成功与否 2、PQfinish (1)作用 (2࿰…...

简单配置,全面保护:HZERO审计服务让安全触手可及
HZERO技术平台,凭借多年企业资源管理实施经验,深入理解企业痛点,为您提供了一套高效易用的审计解决方案。这套方案旨在帮助您轻松应对企业开发中的审计挑战,确保业务流程的合规性和透明度。 接下来,我将为大家详细介绍…...

HCIA-Access V2.5_4_1_1路由协议基础_IP路由表
大型网络的拓扑结构一般会比较复杂,不同的部门,或者总部和分支可能处在不同的网络中,此时就需要使用路由器来连接不同的网络,实现网络之间的数据转发。 本章将介绍路由协议的基础知识、路由表的分类、静态路由基础与配置、VLAN间…...

Spring IOC 和 AOP的学习笔记
Spring框架是java开发行业的标准 Spring全家桶 Web:Spring Web MVC/Spring MVC、Spring Web Flux 持久层:Spring Data / Spring Data JPA 、Spring Data Redis 、Spring Data MongoDB 安全校验:Spring Security 构建工程脚手架ÿ…...

二七(vue2-03)、生命周期四个阶段及八个钩子、工程化开发和脚手架、组件注册、拆分组件
1. 生命周期 1.1 生命周期四个阶段 <!-- Vue生命周期:一个Vue实例从 创建 到 销毁 的整个过程。生命周期四个阶段:① 创建 ② 挂载 ③ 更新 ④ 销毁1.创建阶段:创建响应式数据2.挂载阶段:渲染模板3.更新阶段:修改…...

[树] 最轻的天平
问题描述 天平的两边有时不一定只能挂物品,还可以继续挂着另一个天平,现在给你一些天平的情况和他们之间的连接关系,要求使得所有天平都能平衡所需物品的总重量最轻。 一个天平平衡当且仅当“左端点的重量 \times 左端点到支点的距离 …...
Linux udev介绍使用
udev udev配置文件匹配键和赋值键操作符解释示例修改udev配置U盘自动挂载Usb卸载SD卡挂载SD卡卸载 udev配置文件 /etc/udev/udev.conf 这个文件通常很短,他可能只是包含几行#开头的注释,然后有几行选项: udev_root“/dev/” udev_rules“/…...
单片机:实现节日彩灯(附带源码)
本项目的目标是通过编程实现几个常见的彩灯效果,包括: 流水灯效果(从左到右或从右到左)闪烁效果(所有灯同时闪烁)渐变效果(灯光从亮到灭,再从灭到亮)定时切换颜色效果&a…...

流程引擎Activiti性能优化方案
流程引擎Activiti性能优化方案 Activiti工作流引擎架构概述 Activiti工作流引擎架构大致分为6层。从上到下依次为工作流引擎层、部署层、业务接口层、命令拦截层、命令层和行为层。 基于关系型数据库层面优化 MySQL建表语句优化 Activiti在MySQL中创建默认字符集为utf8&…...

【爬虫一】python爬虫基础合集一
【爬虫一】python爬虫基础合集一 1. 网络请求了解1.1. 请求的类型1.2. 网络请求协议1.3. 网络请求过程简单图解1.4. 网络请求Headers(其中的关键字释义):请求头、响应头 2. 网络爬虫的基本工作节点2.1. 了解简单网络请求获取响应数据的过程所涉及要点 1. 网络请求了…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...

[论文阅读]TrustRAG: Enhancing Robustness and Trustworthiness in RAG
TrustRAG: Enhancing Robustness and Trustworthiness in RAG [2501.00879] TrustRAG: Enhancing Robustness and Trustworthiness in Retrieval-Augmented Generation 代码:HuichiZhou/TrustRAG: Code for "TrustRAG: Enhancing Robustness and Trustworthin…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)
引言 在嵌入式系统中,用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例,介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单,执行相应操作,并提供平滑的滚动动画效果。 本文设计了一个…...