[インデックス 11409] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける Tx.Query メソッドのバグを修正するものです。具体的には、トランザクション内で実行されるクエリが、結果セット(Rows)が完全に処理される前に、関連するプリペアドステートメント(Stmt)を誤って閉じてしまう問題を解決します。
コミット
commit bcb976c5b277b30dab6e771659c01bddec8c8a72
Author: Blake Mizerany <blake.mizerany@gmail.com>
Date: Wed Jan 25 17:49:30 2012 -0800
database/sql: fix Tx.Query
Fixes #2784
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5574073
---
src/pkg/database/sql/sql.go | 7 +++++--
src/pkg/database/sql/sql_test.go | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/src/pkg/database/sql/sql.go b/src/pkg/database/sql/sql.go
index 70499b9a95..7e226b17dc 100644
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -556,8 +556,11 @@ func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) {
if err != nil {
return nil, err
}
- defer stmt.Close()
- return stmt.Query(args...)
+ rows, err := stmt.Query(args...)
+ if err == nil {
+ rows.closeStmt = stmt
+ }
+ return rows, err
}
// QueryRow executes a query that is expected to return at most one row.
diff --git a/src/pkg/database/sql/sql_test.go b/src/pkg/database/sql/sql_test.go
index 3fb137eb24..08db6d38ff 100644
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -311,6 +311,40 @@ func TestTxStmt(t *testing.T) {
}
}
+// Issue: http://golang.org/issue/2784
+// This test didn't fail before because we got luckly with the fakedb driver.
+// It was failing, and now not, in github.com/bradfitz/go-sql-test
+func TestTxQuery(t *testing.T) {
+ db := newTestDB(t, "")
+ defer closeDB(t, db)
+ exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool")
+ exec(t, db, "INSERT|t1|name=Alice")
+
+ tx, err := db.Begin()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tx.Rollback()
+
+ r, err := tx.Query("SELECT|t1|name|")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !r.Next() {
+ if r.Err() != nil {
+ t.Fatal(r.Err())
+ }
+ t.Fatal("expected one row")
+ }
+
+ var x string
+ err = r.Scan(&x)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
// Tests fix for issue 2542, that we release a lock when querying on
// a closed connection.
func TestIssue2542Deadlock(t *testing.T) {
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bcb976c5b277b30dab6e771659c01bddec8c8a72
元コミット内容
database/sql: fix Tx.Query
Fixes #2784
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5574073
変更の背景
このコミットは、Go言語の database/sql パッケージにおける Tx.Query メソッドのバグを修正するために行われました。コミットメッセージに Fixes #2784 とあるように、Issue 2784で報告された問題に対応しています。
当時の Tx.Query の実装では、内部的にプリペアドステートメント(Stmt)を作成し、その Stmt に対してクエリを実行していました。しかし、defer stmt.Close() という記述があったため、Tx.Query メソッドが終了する際に、結果セット(Rows)が完全に読み取られる前に Stmt が閉じられてしまう可能性がありました。
database/sql パッケージの設計では、Rows オブジェクトがクエリ結果をイテレートする間、関連する Stmt や基盤となるデータベース接続をオープンに保つ必要があります。Stmt が prematurely に閉じられると、Rows からデータを読み取ろうとした際にエラーが発生したり、予期せぬ動作を引き起こしたりする可能性がありました。
追加されたテストケース TestTxQuery は、この問題を再現するために書かれました。テストコードのコメントにもあるように、以前は「fakedb driver のおかげでたまたま失敗しなかった」が、github.com/bradfitz/go-sql-test では失敗していた、という状況が示唆されています。これは、特定のデータベースドライバーやテスト環境において、このバグが顕在化していたことを意味します。
(注記:コミットが2012年のものであり、当時のIssueトラッカーの記録が古いため、現在のWeb検索では golang/go リポジトリのIssue 2784に関する直接的な情報は得られませんでした。検索結果は golang/vscode-go の異なるIssueを指していました。これは、Issue番号が異なるリポジトリ間で重複したり、時間の経過とともに情報が整理されたりするためによくあることです。しかし、コミットメッセージとコードの変更内容から、問題の性質は明確に読み取れます。)
前提知識の解説
このコミットを理解するためには、Go言語の database/sql パッケージの基本的な概念を理解しておく必要があります。
database/sqlパッケージ: Go言語でSQLデータベースを操作するための標準パッケージです。データベースドライバーとアプリケーションの間の抽象化レイヤーを提供します。DB(データベース接続プール):sql.Openで取得されるオブジェクトで、データベースへの接続プールを管理します。Tx(トランザクション):DB.Begin()またはDB.BeginTx()で開始されるデータベーストランザクションを表すオブジェクトです。トランザクション内の操作は、Commit()またはRollback()が呼び出されるまで、アトミックに扱われます。Stmt(プリペアドステートメント):DB.Prepare()またはTx.Prepare()で作成されるプリペアドステートメントを表すオブジェクトです。SQLクエリを事前に準備(プリコンパイル)しておくことで、繰り返し実行するクエリのパフォーマンスを向上させたり、SQLインジェクション攻撃を防いだりするのに役立ちます。StmtはClose()メソッドを持ち、リソースを解放します。Queryメソッド:DBまたはTxオブジェクトに対してSQLクエリを実行し、結果セット(Rows)を返すメソッドです。Rows(結果セット):Queryメソッドが返すオブジェクトで、クエリの結果行をイテレートするために使用されます。Next()メソッドで次の行に進み、Scan()メソッドで現在の行のデータをGoの変数にスキャンします。RowsはClose()メソッドを持ち、結果セットに関連するリソースを解放します。Rowsが閉じられると、関連するStmtも閉じられるべきです。deferステートメント: Go言語のキーワードで、deferに続く関数呼び出しを、その関数がリターンする直前に実行するようにスケジュールします。リソースの解放(ファイル、ロック、データベース接続など)によく使用されます。
技術的詳細
このコミットの核心は、Tx.Query メソッドにおける Stmt のライフサイクル管理の修正にあります。
修正前の Tx.Query メソッドの関連部分は以下のようになっていました。
func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, err
}
defer stmt.Close() // ここが問題
return stmt.Query(args...)
}
ここで問題となっていたのは defer stmt.Close() です。
defer ステートメントは、Tx.Query 関数が終了する直前に stmt.Close() を実行するようにスケジュールします。しかし、stmt.Query(args...) が *Rows オブジェクトを返した後、Tx.Query 関数がすぐにリターンしてしまうと、Rows オブジェクトがまだアクティブな状態で stmt.Close() が呼び出されてしまう可能性がありました。
database/sql パッケージの設計では、Rows オブジェクトは、その結果セットを読み取る間、関連する Stmt オブジェクト(および基盤となるデータベース接続)がオープンであることを期待します。Stmt が Rows の読み取りが完了する前に閉じられてしまうと、Rows.Next() や Rows.Scan() を呼び出した際に「ステートメントが閉じられている」といったエラーが発生したり、データが正しく取得できなかったりする原因となります。
この修正では、defer stmt.Close() を削除し、代わりに Rows オブジェクトに Stmt オブジェクトへの参照を保持させるように変更しました。
修正後のコードは以下のようになります。
func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, err
}
// defer stmt.Close() は削除された
rows, err := stmt.Query(args...)
if err == nil {
rows.closeStmt = stmt // Rows オブジェクトに Stmt への参照を保持させる
}
return rows, err
}
この変更により、Stmt は Tx.Query メソッドが終了してもすぐに閉じられることはありません。代わりに、stmt.Query() が返す Rows オブジェクトが、その内部フィールド closeStmt を通じて Stmt への参照を保持します。
Rows オブジェクトは、その Close() メソッドが呼び出されたとき(または、すべての行が読み取られたとき、あるいはエラーが発生したときなど、Rows のライフサイクルが終了したとき)に、closeStmt に保持されている Stmt を適切に閉じます。これにより、Stmt のライフサイクルが Rows のライフサイクルと同期され、Rows がアクティブな間は Stmt がオープンに保たれることが保証されます。
この修正は、database/sql パッケージの堅牢性を高め、トランザクション内でクエリを実行する際の潜在的なバグを防ぐ上で非常に重要です。
コアとなるコードの変更箇所
src/pkg/database/sql/sql.go ファイルの Tx.Query メソッドが変更されました。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -556,8 +556,11 @@ func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) {
if err != nil {
return nil, err
}
- defer stmt.Close()
- return stmt.Query(args...)
+ rows, err := stmt.Query(args...)
+ if err == nil {
+ rows.closeStmt = stmt
+ }
+ return rows, err
}
また、この修正の正しさを検証するために、src/pkg/database/sql/sql_test.go に TestTxQuery という新しいテストケースが追加されました。
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -311,6 +311,40 @@ func TestTxStmt(t *testing.T) {
}
}
+// Issue: http://golang.org/issue/2784
+// This test didn't fail before because we got luckly with the fakedb driver.
+// It was failing, and now not, in github.com/bradfitz/go-sql-test
+func TestTxQuery(t *testing.T) {
+ db := newTestDB(t, "")
+ defer closeDB(t, db)
+ exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool")
+ exec(t, db, "INSERT|t1|name=Alice")
+
+ tx, err := db.Begin()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tx.Rollback()
+
+ r, err := tx.Query("SELECT|t1|name|")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !r.Next() {
+ if r.Err() != nil {
+ t.Fatal(r.Err())
+ }
+ t.Fatal("expected one row")
+ }
+
+ var x string
+ err = r.Scan(&x)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
// Tests fix for issue 2542, that we release a lock when querying on
// a closed connection.
func TestIssue2542Deadlock(t *testing.T) {
コアとなるコードの解説
変更の核心は、Tx.Query メソッド内で defer stmt.Close() を削除し、代わりに rows.closeStmt = stmt という行を追加した点です。
-
defer stmt.Close()の削除:- 以前は、
Tx.Query関数が終了する際に、内部で作成されたプリペアドステートメントstmtがdeferによって閉じられるようにスケジュールされていました。 - しかし、
stmt.Query()が返すRowsオブジェクトがまだアクティブな状態で、Tx.Queryがリターンしてしまうと、Rowsが結果を読み取る前にstmtが閉じられてしまう可能性がありました。 Rowsは、そのライフサイクル中にstmtがオープンであることを前提としているため、これは問題を引き起こす可能性がありました。
- 以前は、
-
rows, err := stmt.Query(args...):- これは、プリペアドステートメント
stmtを使って実際にクエリを実行し、結果セットrowsを取得する部分です。
- これは、プリペアドステートメント
-
if err == nil { rows.closeStmt = stmt }:- クエリの実行が成功した場合(
errがnilの場合)にのみ、この処理が実行されます。 rows.closeStmt = stmtは、Rowsオブジェクトの内部フィールドcloseStmtに、現在使用しているStmtオブジェクトへの参照を代入しています。- この
closeStmtフィールドは、Rowsオブジェクトが自身のClose()メソッドが呼び出されたときに、関連するStmtを適切に閉じるために使用されます。 - これにより、
StmtのライフサイクルがRowsのライフサイクルに結びつけられ、Rowsが結果セットを処理している間はStmtが確実にオープンに保たれるようになります。Rowsが閉じられたときに初めてStmtも閉じられるため、リソースの適切な管理とバグの回避が実現されます。
- クエリの実行が成功した場合(
この修正により、Tx.Query を使用してトランザクション内でクエリを実行する際に、結果セットの読み取り中にプリペアドステートメントが予期せず閉じられるという問題が解決されました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/bcb976c5b277b30dab6e771659c01bddec8c8a72
- Go CL (Code Review): https://golang.org/cl/5574073 (現在はGoの新しいコードレビューシステムにリダイレクトされる可能性があります)
参考にした情報源リンク
- 上記のGitHubコミット情報
- Go言語
database/sqlパッケージのドキュメント (当時のバージョンに準拠) - Go言語の
deferステートメントに関する一般的な知識 database/sqlパッケージにおけるRowsとStmtのライフサイクル管理に関する一般的な知識- Issue 2784に関するWeb検索は、当時の正確な情報を見つけることができませんでした。これは、Issueトラッカーの変更や、古いIssueがアーカイブされることによるものです。しかし、コミットメッセージとコードの変更内容から、問題の性質は明確に理解できました。