Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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) を引数として受け取ります。errnil でない場合、putConn はその接続が不良であると判断し、プールに戻さずに破棄するなどの適切な処理を行うことが期待されます。

技術的詳細

このコミットの技術的な核心は、Go言語の defer ステートメントにおける引数の評価タイミングと、それによって引き起こされるバグの修正方法にあります。

元のコードでは、prepare メソッドと exec メソッドの内部で、以下のように defer ステートメントが使用されていました。

defer db.putConn(ci, err)

ここで err は、prepareexec 関数内で宣言されたローカル変数です。この defer ステートメントが実行される時点(つまり、prepareexec 関数が開始された直後)で、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)の最新の値が評価されます。

つまり、prepareexec 関数がリターンする直前にこの無名関数が実行される際、その時点での 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 に正しく伝達され、不良な接続がコネクションプールに再利用されることを防ぐことができます。

関連リンク

参考にした情報源リンク