[インデックス 16183] ファイルの概要
このコミットは、Go言語の database/sql
パッケージにおける重要なバグ修正です。具体的には、Rows
オブジェクトがクローズされる際に、関連する driver.Stmt
(ステートメント) がクローズされる前に、そのステートメントが使用していた driver.Conn
(データベース接続) がコネクションプールに解放されてしまう競合状態を解消します。これにより、一部のデータベースドライバー(特に lib/pq
のような、Stmt.Close
時にネットワーク通信を行うもの)で発生していた、解放済みコネクション上での不正な操作によるパニックやエラーを防ぎます。
コミット
commit 36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Apr 15 14:06:41 2013 -0700
database/sql: close driver Stmt before releasing Conn
From the issue, which describes it as well as I could:
database/sql assumes that driver.Stmt.Close does not need the
connection.
see database/sql/sql.go:1308:
This puts the Rows' connection back into the idle pool, and
then calls the driver.Stmt.Close method of the Stmt it belongs
to. In the postgresql driver implementation
(https://github.com/lib/pq), Stmt.Close communicates with the
server (on the connection that was just put back into the idle
pool). Most of the time, this causes no problems, but if
another goroutine makes a query at the right (wrong?) time,
chaos results.
In any case, traffic is being sent on "free" connections
shortly after they are freed, leading to race conditions that
kill the driver code.
Fixes #5283
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8633044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
元コミット内容
commit 36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Apr 15 14:06:41 2013 -0700
database/sql: close driver Stmt before releasing Conn
From the issue, which describes it as well as I could:
database/sql assumes that driver.Stmt.Close does not need the
connection.
see database/sql/sql.go:1308:
This puts the Rows' connection back into the idle pool, and
then calls the driver.Stmt.Close method of the Stmt it belongs
to. In the postgresql driver implementation
(https://github.com/lib/pq), Stmt.Close communicates with the
server (on the connection that was just put back into the idle
pool). Most of the time, this causes no problems, but if
another goroutine makes a query at the right (wrong?) time,
chaos results.
In any case, traffic is being sent on "free" connections
shortly after they are freed, leading to race conditions that
kill the driver code.
Fixes #5283
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8633044
変更の背景
Go言語の database/sql
パッケージは、データベース操作のための汎用的なインターフェースを提供します。このパッケージは、具体的なデータベースドライバー(例: PostgreSQL用の lib/pq
、MySQL用の go-sql-driver/mysql
など)と連携して動作します。
問題は、Rows
オブジェクト(クエリ結果の行をイテレートするために使用される)がクローズされる際の内部処理にありました。以前の実装では、Rows.Close()
メソッド内で、まず Rows
が使用していたデータベース接続 (driver.Conn
) をコネクションプールに解放し、その後で関連する driver.Stmt
(プリペアドステートメントなど) をクローズしようとしていました。
しかし、database/sql
パッケージの設計上の仮定として、「driver.Stmt.Close()
メソッドは、そのステートメントが元々関連付けられていたデータベース接続を必要としない」というものがありました。この仮定は、多くのデータベースドライバーでは問題になりませんでしたが、lib/pq
のような一部のPostgreSQLドライバーの実装では、Stmt.Close()
が実際にデータベースサーバーとの通信を伴う場合がありました。例えば、プリペアドステートメントをサーバー側で明示的に解放するプロトコル操作を行う場合などです。
この状況下で、driver.Conn
がコネクションプールに解放された直後に driver.Stmt.Close()
が呼び出されると、以下の競合状態が発生する可能性がありました。
Rows.Close()
がdriver.Conn
をアイドルプールに返却する。- ほぼ同時に、別のゴルーチンがコネクションプールからその
driver.Conn
を取得し、新しいクエリを実行し始める。 - その間に、元の
Rows.Close()
の処理が続き、解放済みであるはずのdriver.Conn
を使用してdriver.Stmt.Close()
が実行される。
結果として、driver.Stmt.Close()
が、すでに別のゴルーチンによって使用されている、あるいは不正な状態にあるコネクションに対して操作を行おうとし、「解放済みコネクション上でのトラフィック送信」や「ドライバーコードのクラッシュ」といった予期せぬ動作やパニックを引き起こしていました。これは、データベース操作の信頼性と安定性を著しく損なう深刻なバグでした。
この問題は Go issue #5283 として報告され、このコミットによって修正されました。
前提知識の解説
このコミットの理解には、以下のGo言語の database/sql
パッケージに関する知識が役立ちます。
database/sql
パッケージ: Go標準ライブラリの一部で、SQLデータベースとの対話のための抽象レイヤーを提供します。これにより、アプリケーションコードは特定のデータベースドライバーに依存することなく、汎用的なインターフェースを通じてデータベースを操作できます。driver.Conn
: データベースへの単一の物理的な接続を表すインターフェースです。database/sql
パッケージは、これらの接続をプールして再利用することで、接続確立のオーバーヘッドを削減します。driver.Stmt
: プリペアドステートメント(Prepared Statement)を表すインターフェースです。SQLクエリを事前にコンパイルし、パラメータをバインドして複数回実行するために使用されます。これにより、パフォーマンスが向上し、SQLインジェクション攻撃を防ぐことができます。Rows
:Query
メソッドの実行結果を表すインターフェースです。結果セットの行をイテレートするために使用され、すべての行が処理された後、またはエラーが発生した場合にはClose()
メソッドを呼び出してリソースを解放する必要があります。- コネクションプーリング (Connection Pooling): データベース接続の確立はコストの高い操作であるため、
database/sql
パッケージは内部的にデータベース接続のプールを管理します。アプリケーションが接続を要求すると、プールから既存のアイドル接続が提供されます。接続が不要になると、プールに返却され、他のリクエストのために再利用されます。 - 競合状態 (Race Condition): 複数のゴルーチン(Goの軽量スレッド)が共有リソース(この場合はデータベース接続)に同時にアクセスし、そのアクセス順序によってプログラムの最終結果が非決定的に変わってしまう状態を指します。競合状態はデバッグが困難なバグの一般的な原因です。
lib/pq
: Go言語でPostgreSQLデータベースに接続するための人気のあるドライバーです。このドライバーは、PostgreSQLのプロトコルに準拠しており、プリペアドステートメントのクローズなど、一部の操作でサーバーとの通信を必要とすることがあります。
技術的詳細
このバグの核心は、database/sql
パッケージの Rows.Close()
メソッドにおけるリソース解放の順序にありました。
-
旧実装の順序:
Rows.Close()
が呼び出されると、まずrs.rowsi.Close()
が実行され、次にrs.releaseConn(err)
が呼び出されて、Rows
が使用していたdriver.Conn
がコネクションプールに返却されていました。その後、もしrs.closeStmt
(関連するdriver.Stmt
) が存在すれば、rs.closeStmt.Close()
が呼び出されていました。// 旧実装の擬似コード func (rs *Rows) Close() error { // ... err := rs.rowsi.Close() rs.releaseConn(err) // (1) コネクションをプールに解放 if rs.closeStmt != nil { rs.closeStmt.Close() // (2) ステートメントをクローズ } // ... }
-
問題の発生:
lib/pq
のような一部のドライバーでは、driver.Stmt.Close()
が実際にデータベースサーバーに対してネットワーク通信を行う必要がありました。この通信は、当然ながらそのステートメントが関連付けられているdriver.Conn
を通じて行われます。 しかし、旧実装ではStmt.Close()
が呼び出される前にConn
がすでにプールに解放されていました。これにより、以下のシナリオが発生し得ました。Conn
がプールに解放された直後、別のゴルーチンがそのConn
をプールから取得し、新しいクエリを実行し始める。- その間に、元の
Rows.Close()
の処理が続き、Stmt.Close()
が実行される。このとき、Stmt.Close()
は、すでに別のゴルーチンによって使用されている、またはプールに返却されたために内部状態がリセットされているConn
を使用しようとする。 - 結果として、不正なコネクション状態での通信試行により、パニックやデータベースドライバーレベルでのエラーが発生しました。コミットメッセージにある「chaos results」や「traffic is being sent on "free" connections shortly after they are freed」という表現は、この競合状態による不安定な挙動を指しています。
-
修正と解決策: このコミットでは、
Rows.Close()
メソッド内のdriver.Stmt.Close()
とrs.releaseConn()
の呼び出し順序を入れ替えることで問題を解決しました。// 新実装の擬似コード func (rs *Rows) Close() error { // ... err := rs.rowsi.Close() if rs.closeStmt != nil { rs.closeStmt.Close() // (1) まずステートメントをクローズ } rs.releaseConn(err) // (2) その後コネクションをプールに解放 // ... }
この変更により、
driver.Stmt.Close()
が呼び出される時点では、関連するdriver.Conn
はまだRows
オブジェクトによって排他的に保持されており、他のゴルーチンに再利用されることはありません。これにより、Stmt.Close()
が安全に、かつ意図した通りにデータベースサーバーとの通信を完了できるようになります。 -
テストの追加と強化: この修正を検証するために、テストコードも強化されました。
src/pkg/database/sql/fakedb_test.go
には、fakeConn.Close()
が失敗した場合にtesting.T.Errorf
を呼び出すsetStrictFakeConnClose
というヘルパー関数が追加されました。これにより、テスト中にコネクションのクローズが失敗した場合に、より厳密にエラーを検出できるようになりました。また、fakeStmt.Close()
には、s.c == nil
やs.c.db == nil
の場合にパニックを起こすチェックが追加され、ステートメントがクローズされる時点でコネクションが有効であることを保証するようになりました。src/pkg/database/sql/sql_test.go
にはTestRowsCloseOrder
という新しいテストケースが追加されました。このテストは、db.SetMaxIdleConns(0)
を設定してコネクションプールを無効にし、setStrictFakeConnClose(t)
を呼び出すことで、コネクションが即座にクローズされ、Stmt.Close
がコネクション解放前に実行されることを厳密に検証します。
これらの変更により、database/sql
パッケージの堅牢性が向上し、特定のデータベースドライバーとの組み合わせで発生していた競合状態が解消されました。
コアとなるコードの変更箇所
src/pkg/database/sql/sql.go
ファイルの Rows.Close()
メソッドにおける変更がコアです。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -1311,10 +1311,10 @@ func (rs *Rows) Close() error {
}
rs.closed = true
err := rs.rowsi.Close()
- rs.releaseConn(err)
if rs.closeStmt != nil {
rs.closeStmt.Close()
}
+ rs.releaseConn(err)
return err
}
コアとなるコードの解説
上記の差分が示すように、Rows.Close()
メソッド内で、rs.releaseConn(err)
の呼び出し位置が変更されました。
-
変更前:
err := rs.rowsi.Close() rs.releaseConn(err) // コネクションをプールに解放 if rs.closeStmt != nil { rs.closeStmt.Close() // その後、ステートメントをクローズ }
この順序では、
rs.closeStmt.Close()
が呼び出される時点で、rs
が保持していたdriver.Conn
はすでにコネクションプールに返却されており、別のゴルーチンによって再利用される可能性がありました。もしrs.closeStmt.Close()
がそのコネクションを必要とする場合(例:lib/pq
のようにサーバーにメッセージを送る場合)、競合状態が発生し、不正なコネクションに対して操作が行われることになります。 -
変更後:
err := rs.rowsi.Close() if rs.closeStmt != nil { rs.closeStmt.Close() // まずステートメントをクローズ } rs.releaseConn(err) // その後、コネクションをプールに解放
この新しい順序では、
rs.closeStmt.Close()
が呼び出される前にrs.releaseConn(err)
が実行されることはありません。つまり、driver.Stmt.Close()
が実行される時点では、関連するdriver.Conn
はまだRows
オブジェクトによって排他的に保持されています。これにより、Stmt.Close()
が安全に、かつ意図した通りに、そのコネクションを通じて必要なクリーンアップ操作(例えば、データベースサーバー上のプリペアドステートメントの解放)を完了できるようになります。すべてのステートメント関連のクリーンアップが完了した後にのみ、コネクションがプールに返却されるため、競合状態が解消されます。
このシンプルな順序の変更が、database/sql
パッケージの安定性と信頼性を大幅に向上させました。
関連リンク
- Go issue #5283: https://github.com/golang/go/issues/5283
参考にした情報源リンク
- GitHub Commit: https://github.com/golang/go/commit/36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
- Go issue #5283: https://github.com/golang/go/issues/5283
- Go CL 8633044: https://golang.org/cl/8633044 (これはコミットメッセージに記載されているGoのコードレビューシステムへのリンクです)
lib/pq
GitHubリポジトリ: https://github.com/lib/pq (コミットメッセージで言及されているPostgreSQLドライバー)- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql (一般的な情報源として)