[インデックス 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.go
L204-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.go
L210-236):closeDBLocked() error
Close() error
finalClose() error
これらのメソッドが追加され、接続のクローズロジックと依存関係管理が実装されました。
-
DB.addDep
およびDB.removeDep
の変更 (src/pkg/database/sql/sql.go
L238-310):DB.addDep
がdb.addDepLocked
を呼び出すように変更され、db.addDepLocked
が追加されました。DB.removeDep
がdb.removeDepLocked
を呼び出すように変更され、db.removeDepLocked
が追加されました。- これらのメソッドの内部ロジックが、依存関係の追跡と
finalCloser
の呼び出しを適切に行うように大幅に修正されました。特に、removeDepLocked
は、依存関係がなくなった場合にのみfinalClose
を呼び出す関数を返すようになりました。
-
DB.Close()
の変更 (src/pkg/database/sql/sql.go
L320-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.go
L365-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.go
L393-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, nil
driverConn
の初期化時にdb
フィールドが設定され、db.addDepLocked(dc, dc)
が呼び出されるようになりました。
-
DB.putConn()
の変更 (src/pkg/database/sql/sql.go
L484-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.go
L528-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.go
L1031-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.go
L1149-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 nil
s.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==