[インデックス 15939] ファイルの概要
このコミットは、Go言語の database/sql パッケージにおける重要なバグ修正と機能改善を目的としています。具体的には、driver.Conn (データベース接続) が、それに関連付けられた driver.Stmt (プリペアドステートメント) がまだ閉じられていない状態で閉じられてしまう問題を解決します。これにより、リソースリークや未定義の動作、さらにはクラッシュが発生する可能性がありました。この変更により、driver.Conn は、その接続に依存するすべての Stmt が閉じられるまで、実際の基盤となる接続を閉じないように制御されるようになります。
コミット
commit 209f6b1d2ca84541505c29c6158cde9d5a0bbd57
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 25 16:50:27 2013 -0700
database/sql: don't close a driver.Conn until its Stmts are closed
Fixes #5046
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8016044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/209f6b1d2ca84541505c29c6158cde9d5a0bbd57
元コミット内容
database/sql: driver.Conn は、その Stmt が閉じられるまで閉じないようにする。
Issue #5046 を修正。
変更の背景
この変更の背景には、Goの標準ライブラリである database/sql パッケージにおけるリソース管理の課題がありました。以前の実装では、DB.Close() や DB.SetMaxIdleConns() などによってデータベース接続 (driver.Conn) が閉じられる際に、その接続から生成されたプリペアドステートメント (driver.Stmt) がまだアクティブであるかどうかを適切に考慮していませんでした。
具体的には、以下のような問題が発生する可能性がありました。
- リソースリーク:
driver.Connが閉じられた後も、それに関連付けられたdriver.Stmtオブジェクトがメモリ上に残り、閉じられた接続を参照し続ける。これにより、データベースドライバによっては、基盤となるデータベースリソースが適切に解放されない可能性がありました。 - 未定義の動作/クラッシュ: 閉じられた接続を参照している
driver.Stmtが後続の操作(例:Stmt.Query(),Stmt.Exec()) を試みると、無効な接続に対して操作を行おうとするため、パニック、セグメンテーション違反、またはその他の予測不能なエラーが発生する可能性がありました。これは特に、接続プールが接続を再利用する際に顕著になる可能性があります。 - 競合状態: 複数のゴルーチンが同じ
driver.Connから生成されたdriver.Stmtを使用している場合、driver.Connが予期せず閉じられると、競合状態やデッドロックを引き起こす可能性がありました。
Issue #5046 (database/sql: must call (*driver.Stmt).Close before correspondent (*driver.Conn).Close) は、この問題を明確に指摘しており、driver.Conn がその Stmt よりも先に閉じられるべきではないという要件を提起していました。このコミットは、この根本的な問題を解決し、database/sql パッケージのリソース管理をより堅牢にするために導入されました。
前提知識の解説
このコミットを理解するためには、Go言語の database/sql パッケージの基本的な概念と、一般的なデータベース接続管理の知識が必要です。
-
database/sqlパッケージ:- Goの標準ライブラリで、データベースにアクセスするための汎用的なインターフェースを提供します。特定のデータベースドライバに依存しない抽象化レイヤーです。
DB構造体: データベースへの抽象的なアクセスポイントを表します。内部的に接続プールを管理し、必要に応じて接続を開閉します。Conn(またはdriver.Conn): データベースへの単一の物理的な接続を表すインターフェースです。DBは複数のConnをプールして管理します。Stmt(またはdriver.Stmt): プリペアドステートメントを表します。SQLクエリを事前にデータベースに送信して準備しておくことで、繰り返し実行する際のパフォーマンスを向上させ、SQLインジェクション攻撃を防ぎます。Stmtは特定のConnに関連付けられます。Tx(またはdriver.Tx): データベーストランザクションを表します。複数のデータベース操作をアトミックに実行するために使用されます。Close()メソッド:DB,Conn,Stmt,Rowsなど、多くのリソース型に存在するメソッドで、使用後にリソースを明示的に解放するために呼び出されます。Goでは、deferキーワードと組み合わせて使用されることが一般的です。
-
リソース管理と依存関係:
- プログラミングにおいて、ファイルハンドル、ネットワークソケット、データベース接続などのリソースは、使用後に適切に解放される必要があります。解放を怠ると、リソースリークが発生し、システムのパフォーマンス低下や不安定化につながります。
- 特に、あるリソースが別のリソースに依存している場合(例:
StmtがConnに依存している)、依存元のリソースが解放される前に依存先のリソースが解放されると、問題が発生します。このコミットは、まさにこの依存関係の管理を改善するものです。
-
sync.Mutex:- Goの標準ライブラリ
syncパッケージに含まれるミューテックス(相互排他ロック)です。複数のゴルーチンが共有リソースに同時にアクセスするのを防ぎ、データ競合を防ぐために使用されます。このコミットでは、driverConnやDBの内部状態を保護するために使用されています。
- Goの標準ライブラリ
-
finalCloserインターフェース:- このコミットで導入された内部的なインターフェースで、
finalClose() errorメソッドを持つ型を定義します。これは、依存関係がすべて解消された後に最終的に閉じられるべきリソースを抽象化するために使用されます。
- このコミットで導入された内部的なインターフェースで、
技術的詳細
このコミットの核心は、database/sql パッケージにおける driver.Conn と driver.Stmt の間の依存関係を明示的に追跡し、管理するメカニズムを導入した点にあります。これにより、driver.Conn が、それに関連付けられた driver.Stmt がすべて閉じられるまで、基盤となるデータベース接続を閉じないように保証されます。
主な技術的変更点は以下の通りです。
-
driverConn構造体の拡張:src/pkg/database/sql/sql.go内のdriverConn構造体に、以下のフィールドが追加されました。db *DB: この接続が属するDBインスタンスへのポインタ。これにより、driverConnからDBの依存関係管理機能にアクセスできるようになります。closed bool: このdriverConnが論理的に閉じられた状態にあるかを示すフラグ。これにより、二重クローズを防ぎます。
driverConnには、closeDBLocked(),Close(),finalClose()という新しいメソッドが追加されました。driverConn.Close(): 外部から呼び出されるクローズメソッド。これは、DBの依存関係管理システムを通じて、このdriverConnが持つすべての依存関係(Stmtなど)が解放された後に、実際の基盤となるdriver.ConnのClose()メソッド (ci.Close()) が呼び出されるようにスケジュールします。driverConn.finalClose(): 実際に基盤となるdriver.Conn(ci) を閉じるメソッド。これは、DBの依存関係管理システムによって、このdriverConnに依存するものがなくなったと判断された場合にのみ呼び出されます。
-
DB構造体における依存関係管理の強化:DB構造体には、dep map[finalCloser]depSetというマップが追加されました。これは、finalCloserインターフェースを実装するオブジェクト(例:driverConn,Stmt)が、他のどのオブジェクトに依存しているかを追跡するためのものです。DB.addDep(x finalCloser, dep interface{})およびDB.removeDep(x finalCloser, dep interface{})メソッドが大幅に改修されました。addDep:xがdepに依存していることを記録します。例えば、StmtがdriverConnに依存している場合、db.addDep(dc, stmt)のように呼び出されます。removeDep:xがdepへの依存関係を解消したことを記録します。Stmtが閉じられた際にdb.removeDep(dc, stmt)のように呼び出されます。removeDepは、xに依存するものがなくなった場合にのみ、x.finalClose()を呼び出すように変更されました。これにより、driverConnは、それに関連付けられたすべてのStmtが閉じられるまで、実際のクローズ処理を遅延させることができます。
-
StmtとdriverConnの間の依存関係の確立:DB.prepare()メソッド内でStmtが作成される際に、db.addDep(stmt, stmt)に加えてdb.addDep(dc, stmt)が呼び出されるようになりました。これは、「このStmtは、このdriverConnに依存している」という関係を明示的に登録します。Stmt.connStmt()メソッド(Stmtが接続を取得する内部メソッド)でも、s.db.addDep(dc, s)が呼び出され、同様の依存関係が登録されます。Stmt.finalClose()メソッド(Stmtが最終的に閉じられる際に呼び出される)内で、s.db.removeDep(v.dc, s)が呼び出されるようになりました。これは、「このStmtは、このdriverConnへの依存関係を解消した」ことを通知します。
-
接続クローズロジックの変更:
DB.Close()およびDB.SetMaxIdleConns()内で、アイドル状態の接続を閉じる際に、直接dc.ci.Close()を呼び出すのではなく、新しく導入されたdc.closeDBLocked()またはdc.Close()を呼び出すように変更されました。これにより、接続のクローズ処理が依存関係管理システムを通じて行われるようになります。DB.putConn()(使用済み接続をプールに戻す) でも、dc.Close()が呼び出されるようになりました。
-
テストケースの追加:
src/pkg/database/sql/sql_test.goにTestCloseConnBeforeStmtsという新しいテストケースが追加されました。このテストは、DB.Prepare()でStmtを作成した後、DB.Close()を呼び出し、その後でStmt.Close()を呼び出すというシナリオをシミュレートします。これにより、driver.ConnがStmtよりも先に閉じられないことを検証し、このコミットによる修正が正しく機能していることを確認します。
これらの変更により、database/sql パッケージは、driver.Conn と driver.Stmt の間のライフサイクル依存関係をより堅牢に管理できるようになり、リソースリークや不正な接続使用による問題を効果的に防止します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/database/sql/sql.go に集中しています。
-
driverConn構造体の変更 (src/pkg/database/sql/sql.goL204-207):type driverConn struct { db *DB // 追加 sync.Mutex // guards following ci driver.Conn closed bool // 追加 }db *DBフィールドとclosed boolフィールドが追加されました。
-
driverConnの新しいメソッド (src/pkg/database/sql/sql.goL210-236):closeDBLocked() errorClose() errorfinalClose() errorこれらのメソッドが追加され、接続のクローズロジックと依存関係管理が実装されました。
-
DB.addDepおよびDB.removeDepの変更 (src/pkg/database/sql/sql.goL238-310):DB.addDepがdb.addDepLockedを呼び出すように変更され、db.addDepLockedが追加されました。DB.removeDepがdb.removeDepLockedを呼び出すように変更され、db.removeDepLockedが追加されました。- これらのメソッドの内部ロジックが、依存関係の追跡と
finalCloserの呼び出しを適切に行うように大幅に修正されました。特に、removeDepLockedは、依存関係がなくなった場合にのみfinalCloseを呼び出す関数を返すようになりました。
-
DB.Close()の変更 (src/pkg/database/sql/sql.goL320-326):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -320,9 +365,7 @@ func (db *DB) Close() error { defer db.mu.Unlock() var err error for _, dc := range db.freeConn { - dc.Lock() - err1 := dc.ci.Close() - dc.Unlock() + err1 := dc.closeDBLocked() // 変更 if err1 != nil { err = err1 }dc.ci.Close()の直接呼び出しがdc.closeDBLocked()に変更されました。
-
DB.SetMaxIdleConns()の変更 (src/pkg/database/sql/sql.goL365-372):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -365,11 +408,7 @@ func (db *DB) SetMaxIdleConns(n int) { dc := db.freeConn[nfree-1] db.freeConn[nfree-1] = nil db.freeConn = db.freeConn[:nfree-1] - go func() { - dc.Lock() - dc.ci.Close() - dc.Unlock() - }() + go dc.Close() // 変更 } }dc.ci.Close()の直接呼び出しがgo dc.Close()に変更されました。
-
DB.conn()の変更 (src/pkg/database/sql/sql.goL393-402):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -393,8 +432,12 @@ func (db *DB) conn() (*driverConn, error) { if err != nil { return nil, err } - dc := &driverConn{ci: ci} + dc := &driverConn{ // 変更 + db: db, // 追加 + ci: ci, + } db.mu.Lock() + db.addDepLocked(dc, dc) // 追加 db.outConn[dc] = true db.mu.Unlock() return dc, nildriverConnの初期化時にdbフィールドが設定され、db.addDepLocked(dc, dc)が呼び出されるようになりました。
-
DB.putConn()の変更 (src/pkg/database/sql/sql.goL484-489):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -484,9 +527,7 @@ func (db *DB) putConn(dc *driverConn, err error) { // statements which are still active? db.mu.Unlock() - dc.Lock() - dc.ci.Close() - dc.Unlock() + dc.Close() // 変更 }dc.ci.Close()の直接呼び出しがdc.Close()に変更されました。
-
DB.prepare()の変更 (src/pkg/database/sql/sql.goL528-534):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -528,6 +569,7 @@ func (db *DB) prepare(query string) (*Stmt, error) { css: []connStmt{{dc, si}}, } db.addDep(stmt, stmt) + db.addDep(dc, stmt) // 追加 db.putConn(dc, nil) return stmt, nil }db.addDep(dc, stmt)が追加され、StmtがdriverConnに依存することを登録します。
-
Stmt.connStmt()の変更 (src/pkg/database/sql/sql.goL1031-1037):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -1031,6 +1073,7 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St if err != nil { return nil, nil, nil, err } + s.db.addDep(dc, s) // 追加 s.mu.Lock() cs = connStmt{dc, si} s.css = append(s.css, cs)s.db.addDep(dc, s)が追加され、StmtがdriverConnに依存することを登録します。
-
Stmt.finalClose()の変更 (src/pkg/database/sql/sql.goL1149-1154):--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -1149,6 +1192,7 @@ func (s *Stmt) Close() error { func (s *Stmt) finalClose() error { for _, v := range s.css { s.db.noteUnusedDriverStatement(v.dc, v.si) + s.db.removeDep(v.dc, s) // 追加 } s.css = nil return nils.db.removeDep(v.dc, s)が追加され、StmtがdriverConnへの依存関係を解消したことを通知します。
-
テストファイルの変更 (
src/pkg/database/sql/fakedb_test.goとsrc/pkg/database/sql/sql_test.go):fakedb_test.goにhookPostCloseConnが追加され、fakeConnのクローズをフックできるようになりました。sql_test.goにTestCloseConnBeforeStmtsが追加され、このコミットの修正を検証するテストケースが実装されました。
コアとなるコードの解説
このコミットのコアとなる変更は、database/sql パッケージがデータベース接続 (driver.Conn) とプリペアドステートメント (driver.Stmt) の間のライフサイクル依存関係をどのように管理するかを根本的に変えるものです。
-
driverConnの強化とfinalCloseメカニズム:- 以前の
driverConnは単に基盤となるdriver.Connインターフェースをラップするだけでした。この変更により、driverConnは自身のDBインスタンスへの参照と、自身が論理的に閉じられたかどうかを示すclosedフラグを持つようになりました。 - 最も重要なのは、
driverConn.Close()とdriverConn.finalClose()の導入です。driverConn.Close()は、外部から接続を閉じようとする際に呼び出されます。しかし、このメソッドはすぐに基盤となる接続を閉じるわけではありません。代わりに、DBの依存関係管理システムに、このdriverConnを閉じる準備ができたことを通知します。driverConn.finalClose()は、実際に基盤となるdriver.Conn(ci) のClose()メソッドを呼び出し、リソースを解放する役割を担います。このメソッドは、DBの依存関係管理システムによって、このdriverConnに依存するすべてのStmtが閉じられたと判断された場合にのみ呼び出されます。これにより、driver.ConnがアクティブなStmtを持つ状態で閉じられることがなくなります。
- 以前の
-
DBにおける依存関係追跡の導入:DB構造体に追加されたdepマップは、finalCloserインターフェースを実装するオブジェクト(driverConnやStmtなど)間の依存関係を追跡するための中心的な役割を果たします。DB.addDep(x, dep)は、「xはdepに依存している」という関係を登録します。例えば、DB.prepare()でStmtが作成される際にdb.addDep(dc, stmt)が呼び出されます。これは、「このStmtは、このdriverConnに依存している」ことを意味します。DB.removeDep(x, dep)は、「xがdepへの依存関係を解消した」ことを通知します。例えば、Stmt.Close()が呼び出される際にdb.removeDep(v.dc, s)が呼び出されます。removeDepのロジックは、xに依存するものがなくなった場合にのみ、x.finalClose()を呼び出すように設計されています。これにより、driverConnは、それに依存するすべてのStmtが閉じられるまで、実際のクローズ処理を「保留」することができます。
-
StmtとdriverConnのライフサイクル連携:DB.prepare()やStmt.connStmt()でStmtが作成されるたびに、そのStmtが使用するdriverConnに対する依存関係がdb.addDep(dc, stmt)を通じて登録されます。Stmt.Close()が呼び出され、Stmt.finalClose()が実行されると、db.removeDep(v.dc, s)が呼び出され、driverConnへの依存関係が解除されます。- この依存関係の登録と解除のメカニズムにより、
DBは各driverConnに関連付けられたアクティブなStmtの数を正確に把握できます。driverConnに依存するStmtが一つもなくなったときに初めて、driverConn.finalClose()が呼び出され、基盤となるデータベース接続が安全に閉じられます。
これらの変更は、database/sql パッケージがデータベース接続をより堅牢かつ安全に管理するための基盤を築きました。これにより、開発者は接続とステートメントのライフサイクルについて過度に心配することなく、データベース操作に集中できるようになります。
関連リンク
- Go Issue #5046: https://golang.org/issue/5046
- Go Code Review 8016044: https://golang.org/cl/8016044
参考にした情報源リンク
- Web search results for "golang.org/issue/5046": https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFhiOVDgepWi1noNBjNHYCuFcHl9fFfVfGRKsLFzORmR3uBAaySW7-4G1apiHHIRfR-557cgVlAj7PCEaarU7wB7nl9Qb4GWw3Ku9PgVDHAHCF-GhItkIx9TSvleDHMlkX2lg==
- goissues.org (related to issue 5046): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFiJ8G8UzZrZxi8n3UgcmHudLdox5nfq9weByL2fRWMi7aKWEclcJ_bbxJjLHNZMcLzBklutT44CkL-fzlb4xoMkDNA8rksIRT0rFvhXRJEjpt4HAEWIs2RfyTKsnlmCwxxBigmLugYzQ==