[インデックス 13682] ファイルの概要
コミット
commit 37b40dab51e31ed246e2cd40b827d26b93cf9003
Author: Julien Schmidt <google@julienschmidt.com>
Date: Thu Aug 23 19:29:47 2012 -0700
database/sql: stop reuse of bad connections
The second parameter for sql.putConn() (err) is always nil. As a result bad
connections are reused, even if the driver returns an driver.ErrBadConn.
Unsing a pointer to err instead achievs the desired behavior.
See http://code.google.com/p/go/issues/detail?id=3777 for more details.
Fixes #3777.
R=golang-dev, dave, bradfitz, jameshuachow, BlakeSGentry
CC=golang-dev
https://golang.org/cl/6348069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/37b40dab51e31ed246e2cd40b827d26b93cf9003
元コミット内容
database/sql: stop reuse of bad connections
The second parameter for sql.putConn() (err) is always nil. As a result bad
connections are reused, even if the driver returns an driver.ErrBadConn.
Unsing a pointer to err instead achievs the desired behavior.
See http://code.google.com/p/go/issues/detail?id=3777 for more details.
Fixes #3777.
変更の背景
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおける重要なバグ修正を目的としています。具体的には、データベース接続が不良状態(driver.ErrBadConn
)になった場合でも、その不良な接続がコネクションプールに再利用されてしまう問題に対処しています。
元の実装では、sql.putConn()
関数に渡される err
パラメータが常に nil
として評価されていました。これは、Goの defer
ステートメントの評価タイミングに起因するもので、defer
に渡された関数の引数は defer
ステートメントが宣言された時点で評価されてしまうためです。結果として、データベース操作中にエラーが発生し、driver.ErrBadConn
が返されたとしても、putConn
には nil
が渡され、不良な接続が健全な接続としてプールに戻されてしまい、後続の操作でその不良な接続が再利用されることで、予期せぬエラーやパフォーマンスの問題を引き起こしていました。
この問題は、GoのIssue #3777として報告されており、このコミットはその問題を解決するために導入されました。
前提知識の解説
Go言語の database/sql
パッケージ
database/sql
パッケージは、Go言語でリレーショナルデータベースを操作するための汎用的なインターフェースを提供します。このパッケージは、特定のデータベースドライバーに依存しない抽象化レイヤーであり、アプリケーションは統一されたAPIを通じて様々なデータベース(MySQL, PostgreSQL, SQLiteなど)とやり取りできます。
主要な概念として以下があります。
DB
: データベースへの接続プールを表す構造体です。複数のデータベース接続を管理し、必要に応じて接続を開放したり再利用したりします。Conn
: データベースへの単一の物理的な接続を表します。driver.Driver
: データベースドライバーが実装すべきインターフェースです。driver.Conn
: ドライバーが提供するデータベース接続のインターフェースです。driver.ErrBadConn
: ドライバーが返す可能性のあるエラーで、接続が不良状態であることを示します。このエラーが返された場合、database/sql
パッケージはその接続を再利用すべきではありません。- コネクションプール: データベース接続の確立はコストの高い操作であるため、
database/sql
は接続をプールして再利用します。これにより、アプリケーションのパフォーマンスが向上します。
Go言語の defer
ステートメント
defer
ステートメントは、Go言語の非常に強力な機能の一つです。defer
に続く関数呼び出しは、その関数がリターンする直前(return
ステートメントの実行後、またはパニック発生時)に実行されることを保証します。
しかし、重要な注意点があります。defer
に渡される関数の引数は、defer
ステートメントが宣言された時点で評価されます。これは、defer
された関数が実際に実行される時点ではありません。
例:
func example() {
i := 0
defer fmt.Println(i) // ここで i は 0 として評価される
i++
return // ここで defer された関数が実行されるが、i は 0 のまま
}
// 出力: 0
この特性が、本コミットで修正されるバグの根本原因でした。
sql.putConn()
関数
database/sql
パッケージ内部で使用される putConn()
関数は、使用済みのデータベース接続をコネクションプールに戻す役割を担っています。この関数は、接続オブジェクト (ci
) とエラー (err
) を引数として受け取ります。err
が nil
でない場合、putConn
はその接続が不良であると判断し、プールに戻さずに破棄するなどの適切な処理を行うことが期待されます。
技術的詳細
このコミットの技術的な核心は、Go言語の defer
ステートメントにおける引数の評価タイミングと、それによって引き起こされるバグの修正方法にあります。
元のコードでは、prepare
メソッドと exec
メソッドの内部で、以下のように defer
ステートメントが使用されていました。
defer db.putConn(ci, err)
ここで err
は、prepare
や exec
関数内で宣言されたローカル変数です。この defer
ステートメントが実行される時点(つまり、prepare
や exec
関数が開始された直後)で、err
の値はまだ nil
です。なぜなら、データベース操作(ci.Prepare(query)
や ci.Exec(dargs...)
)はまだ実行されておらず、エラーが発生していないからです。
Goの defer
のセマンティクスにより、db.putConn(ci, err)
の err
引数は、defer
ステートメントが定義された時点で評価され、その時点での err
の値(つまり nil
)が putConn
に渡されるように「固定」されてしまいます。
その後、ci.Prepare(query)
や ci.Exec(dargs...)
の呼び出しでエラー(例えば driver.ErrBadConn
)が発生し、err
変数の値が nil
以外に更新されたとしても、defer
された putConn
に渡される err
の値は、最初に評価された nil
のままです。
結果として、putConn
関数は常に nil
のエラーを受け取り、接続が不良であるにもかかわらず、健全な接続としてコネクションプールに再利用してしまっていました。
この問題を解決するために、コミットでは defer
ステートメントを以下のように変更しました。
defer func() {
db.putConn(ci, err)
}()
この変更のポイントは、defer
に直接関数呼び出しを渡すのではなく、無名関数(クロージャ)を渡している点です。無名関数自体は defer
ステートメントが定義された時点で作成されますが、その無名関数が実行される時点で、その無名関数がキャプチャしている変数(この場合は err
)の最新の値が評価されます。
つまり、prepare
や exec
関数がリターンする直前にこの無名関数が実行される際、その時点での err
変数の値(データベース操作で発生した実際のエラー、または nil
)が db.putConn(ci, err)
に渡されるようになります。これにより、driver.ErrBadConn
のようなエラーが発生した場合には、putConn
がそのエラーを受け取り、不良な接続を適切に処理(プールに戻さないなど)できるようになりました。
この修正は、Go言語の defer
とクロージャの挙動を深く理解していることを示すものであり、Goの標準ライブラリの堅牢性を高める上で非常に重要です。
コアとなるコードの変更箇所
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -311,7 +311,10 @@ func (db *DB) prepare(query string) (stmt *Stmt, err error) {
if err != nil {
return nil, err
}
- defer db.putConn(ci, err)
+ defer func() {
+ db.putConn(ci, err)
+ }()
+
si, err := ci.Prepare(query)
if err != nil {
return nil, err
@@ -342,7 +345,9 @@ func (db *DB) exec(query string, args []interface{}) (res Result, err error) {
if err != nil {
return nil, err
}
- defer db.putConn(ci, err)
+ defer func() {
+ db.putConn(ci, err)
+ }()
if execer, ok := ci.(driver.Execer); ok {
dargs, err := driverArgs(nil, args)
コアとなるコードの解説
変更は src/pkg/database/sql/sql.go
ファイルの DB
型の prepare
メソッドと exec
メソッドに適用されています。
prepare
メソッドの変更
元のコード:
defer db.putConn(ci, err)
変更後:
defer func() {
db.putConn(ci, err)
}()
exec
メソッドの変更
元のコード:
defer db.putConn(ci, err)
変更後:
defer func() {
db.putConn(ci, err)
}()
両方のメソッドで、defer
ステートメントに直接 db.putConn(ci, err)
を渡す代わりに、func() { db.putConn(ci, err) }()
という無名関数(クロージャ)を渡すように修正されています。
この修正により、putConn
が実際に呼び出される(つまり、prepare
または exec
関数が終了する)時点で、そのスコープ内の err
変数の最新の値が評価され、putConn
に渡されるようになります。これにより、データベース操作中に発生した実際のエラー(driver.ErrBadConn
など)が putConn
に正しく伝達され、不良な接続がコネクションプールに再利用されることを防ぐことができます。
関連リンク
- Go Issue #3777: https://github.com/golang/go/issues/3777
- Go CL 6348069: https://golang.org/cl/6348069
参考にした情報源リンク
- https://github.com/golang/go/issues/3777
- https://github.com/golang/go/commit/37b40dab51e31ed246e2cd40b827d26b93cf9003
- Go言語の
defer
ステートメントに関する一般的な情報源 (例: Go公式ドキュメント、Go言語の書籍など) - Go言語の
database/sql
パッケージに関する一般的な情報源 (例: Go公式ドキュメント、Go言語の書籍など) - Go言語のクロージャに関する一般的な情報源 (例: Go公式ドキュメント、Go言語の書籍など)