【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.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()
  • 禁用野生協程,控制協程生命周期

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/63414.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/63414.shtml
英文地址,請注明出處:http://en.pswp.cn/web/63414.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

信號槽【QT】

文章目錄 對象樹字符集信號槽QT坐標系信號與槽connect自定義槽自定義信號disconnect 對象樹 #ifndef MYLABEL_H #define MYLABEL_H#include<QLabel> class MyLabel : public QLabel { public:// 構造函數使用帶 QWidget* 版本的.// 確保對象能夠加到對象樹上MyLabel(QWi…

寫SQL太麻煩?免費搭建 Text2SQL 應用,智能寫 SQL | OceanBase AI 實踐

自OceanBase 4.3.3版本推出以來&#xff0c;向量檢索的能力受到了很多客戶的關注&#xff0c;也紛紛表達希望OB能拓展更多 多模數據庫大模型 的AI應用實踐。 在上篇文章 &#x1f449; OceanBase LLM&#xff0c;免費構建你的專屬 AI 助手 &#xff0c;我們介紹了如何去搭建一…

400G/800G光模塊崛起:AI時代的網絡基礎設施革命

隨著AI技術的不斷成熟&#xff0c;各行各業都在大規模投入AI。醫療行業通過AI技術實現了更精準的診斷和治療&#xff1b;金融行業通過AI技術提高了風險管理能力&#xff1b;制造行業通過AI技術優化了生產流程&#xff1b;娛樂行業通過AI技術創造了更加豐富的用戶體驗。AI在醫療…

Dalsa線陣CCD相機使用開發手冊

要使用Dalsa工業相機進行二次開發&#xff0c;看用戶開發手冊順便做下筆記&#xff1a;&#xff08;歡迎加QQ討論&#xff1a;77248031&#xff0c; 或QQ群&#xff1a;585068192&#xff09; 由于“本公主”用的.NET開發&#xff0c;軟件支持只翻譯了手冊中.NET部分&#xff0…

C++特殊類設計(單例模式等)

目錄 引言 1.請設計一個類&#xff0c;不能被拷貝 2. 請設計一個類&#xff0c;只能在堆上創建對象 為什么設置實例的方法為靜態成員呢 3. 請設計一個類&#xff0c;只能在棧上創建對象 4. 請設計一個類&#xff0c;不能被繼承 5. 請設計一個類&#xff0c;只能創建一個對…

分布式系統架構:服務容錯

1.為什么需要容錯 分布式系統的本質是不可靠的&#xff0c;一個大的服務集群中&#xff0c;程序可能崩潰、節點可能宕機、網絡可能中斷&#xff0c;這些“意外情況”其實全部都在“意料之中”。故障的發生是必然的&#xff0c;所以需要設計一套健壯的容錯機制來應對這些問題。 …

【Latex手冊】自用

收錄Latex使用文檔/工具 個人使用時候的tips&#xff0c;僅供個人使用 核心網頁&#xff1a;LaTeX 工作室 【1】首頁 | LaTeX 知識庫 &#xff08;有詳細的入門教程&#xff09; 【2】LaTeX工作室 - LaTeX工作室&#xff08;一些模板&#xff09; 【3】LaTeX 工作室 &…

Pytorch應用實戰(1)- 基于YOLO的視頻人臉馬賽克處理

免費鏈接: Blogger(需翻Q), Github 文章目錄 本文介紹給圖片的人臉打碼給視頻的人臉打碼本文介紹 YoloV11(Github)提供了非常方便的API幫助用戶實現目標檢測(detect)、語義分割(segement)、肢體識別(Pose)等功能。 本文將基于YoloV11的目標檢測來實現一個視頻人臉馬…

[IT項目管理]九.項目質量管理

九&#xff0e;項目質量管理 9.1項目質量管理的重要性 對于很多IT項目的差勁&#xff0c;大多數人只可以忍受。項目質量管理是IT項目管理的重要組成部分&#xff0c;對于提高項目成功率、降低項目成本、提升客戶滿意度至關重要。盡管很多人對IT項目的質量問題感到無奈&#x…

【Threejs】從零開始(六)--GUI調試開發3D效果

請先完成前置步驟再進行下面操作&#xff1a;【Threejs】從零開始&#xff08;一&#xff09;--創建threejs應用-CSDN博客 一.GUI界面概述 GUI&#xff08;Graphical User Interface&#xff09;指的是圖形化用戶界面&#xff0c;廣泛用在各種程序的上位機&#xff0c;能夠通過…

ffmpeg-SDL顯示BMP

效果圖如下 本文主要將我們通過創建窗口、渲染上下文工具、紋理工具、矩形框工具&#xff1b;其需要主要的是&#xff1a;首先我們在顯示BMP時&#xff0c;需要先創建好窗口&#xff0c;再使用渲染工具對窗口進行格式刷&#xff0c;使用紋理工具和渲染工具配合進行BMP圖片顯示…

多音軌視頻使用FFmpeg刪除不要音軌方法

近期給孩子找宮崎駿動畫&#xff0c;但是有很多是多音軌視頻但是默認的都是日語&#xff0c;電視上看沒辦法所以只能下載后刪除音軌文件只保留中文。 方法分兩步&#xff0c;先安裝FFmpeg在轉文件即可。 第一步FFmpeg安裝 FFmpeg是一個開源項目&#xff0c;包含了處理視頻的…

基礎二分查找總結題-單峰序列2類做法

&#x1f330;單峰序列題目描述 晴問算法 題目描述&#xff1a; 單峰序列是指&#xff0c;在這個序列中存在一個位置&#xff0c;滿足這個位置的左側&#xff08;含該位置&#xff09;是嚴格遞增的、右側&#xff08;含該位置&#xff09;是嚴格遞減的&#xff0c;這個位置被…

【SH】Ubuntu Server 24搭建Web服務器訪問Python程序研發筆記

文章目錄 說個問題寫個方案一、安裝Ubuntu Server二、安裝Web服務器采用Nginx服務器 三、安裝Python及依賴創建項目虛擬環境 四、安裝Python Web框架采用Flask框架創建和運行Flask應用&#xff08;以后的重點&#xff09; 五、安裝WSGI服務器采用Gunicorn 六、配置Nginx七、驗證…

Vue3 重置ref或者reactive屬性值

需要重新定義一個對象綁定復制給原對象 。 實例代碼: const data () > ({groupId: ,groupCode: ,groupName: ,groupType: ,});const formData ref(data());//重置對象值 const reset()>{Object.assign(formData, data()…

C#速成(GID+圖形編程)

常用類 類說明Brush填充圖形形狀,畫刷GraphicsGDI繪圖畫面&#xff0c;無法繼承Pen定義繪制的對象直線等&#xff08;顏色&#xff0c;粗細&#xff09;Font定義文本格式&#xff08;字體&#xff0c;字號&#xff09; 常用結構 結構說明Color顏色Point在平面中定義點Rectan…

vue iframe進行父子頁面通信并切換URL

使用通義千問提問后得到一個很好的示例。 需求是2個項目需要使用同一個面包屑進行跳轉&#xff0c;其中一個是iframe所在的項目&#xff0c;另一個需要通過地址訪問。通過 window.parent.postMessage &#xff0c;幫助 <iframe> 內嵌入的子頁面和其父頁面之間進行跨域通…

誰說C比C++快?

看到這個問題&#xff0c;我我得說&#xff1a;這事兒沒有那么簡單。 1. 先把最大的誤區打破 "C永遠比C快" —— 某位1990年代的程序員 這種說法就像"自行車永遠比汽車省油"一樣荒謬。我們來看個例子&#xff1a; // C風格 char* str (char*)malloc(100…

【ADS射頻電路學習筆記】1. ADS基本操作

下面介紹ADS中主要仿真器的使用 1. 直流仿真 直流仿真器在控制面板的simulator-dc 直流仿真器 但是ADS自帶有很多仿真器&#xff0c;可以直接來調用 選用晶體管電流掃描的模板 就可以輸出模板 然后調入晶體管模型 然后要設置掃描的電壓&#xff0c;選擇dc仿真器對vds進行掃描…

CSS學習記錄12

CSS浮動 CSSfloat屬性規定元素如何浮動 CSSclear屬性規定哪些元素可以在清除的元素旁邊以及在哪一側浮動。 float屬性 float屬性用于定位和格式化內容&#xff0c;例如讓圖像向左浮動到容器的文本那里。 float屬性可以設置以下值之一&#xff1a; left - 元素浮動到其容器…