[インデックス 15224] ファイルの概要
コミット
commit 8f2430a533964f7bab11525d0baa30149b019e04
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 13 12:00:03 2013 -0800
database/sql: add currently-disabled broken test
Update #3865
R=golang-dev, alex.brainman, nightlyone
CC=golang-dev
https://golang.org/cl/7324051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8f2430a533964f7bab11525d0baa30149b019e04
元コミット内容
このコミットは、Go言語の標準ライブラリであるdatabase/sqlパッケージに、現在無効化されている(スキップされている)テストケースを追加するものです。コミットメッセージには「add currently-disabled broken test」とあり、これは意図的に失敗することが分かっているテストを追加し、将来の修正のためにその問題を追跡するためのものです。具体的には、golang.org/issue/3865で報告された問題に関連しています。
変更の背景
このコミットの背景には、Go言語のdatabase/sqlパッケージにおける特定のバグ、すなわちgolang.org/issue/3865が存在します。このIssueは、「sql.Stmt(プリペアドステートメント)をsql.Rows(クエリ結果)を完全に読み取る前にクローズすると、デッドロックやリソースリークが発生する可能性がある」という問題を示唆しています。
通常、database/sqlパッケージでは、Queryメソッドでsql.Rowsが返された後、そのRowsオブジェクトを完全にイテレートするか、明示的にCloseメソッドを呼び出すことで、関連するリソース(データベース接続上のステートメントハンドルなど)が解放されます。しかし、この問題では、Rowsがまだアクティブな状態でStmtをクローズしようとすると、予期せぬ動作が発生していました。
このコミットは、この既知のバグを再現するためのテストケースを追加することで、問題の存在を明確にし、将来的にこのバグが修正された際に、その修正が正しく機能するかどうかを検証できるようにすることを目的としています。テストはt.Skipで無効化されており、これはテストが意図的に失敗すること、またはまだ修正されていないバグをテストしていることを示しています。
前提知識の解説
Go言語のdatabase/sqlパッケージ
database/sqlパッケージは、Go言語でSQLデータベースを操作するための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含まず、データベース固有の機能は外部のドライバーパッケージ(例: github.com/go-sql-driver/mysql、github.com/lib/pqなど)によって提供されます。
主要な概念は以下の通りです。
sql.DB: データベースへの接続プールを表します。複数のゴルーチンから安全に利用できます。sql.Stmt: プリペアドステートメントを表します。SQLインジェクションを防ぎ、同じクエリを複数回実行する際のパフォーマンスを向上させます。DB.Prepare()またはTx.Prepare()で作成されます。sql.Rows: クエリの結果セットを表します。Stmt.Query()またはDB.Query()で返されます。結果セットの各行をイテレートするために使用されます。sql.Tx: データベーストランザクションを表します。複数の操作をアトミックに実行するために使用されます。
Go言語のテストフレームワーク
Go言語には、標準ライブラリとしてtestingパッケージが提供されており、これを使ってユニットテストやベンチマークテストを記述します。
testing.T: テスト関数に渡される型で、テストの失敗を報告したり、ログを出力したり、テストをスキップしたりするためのメソッドを提供します。t.Fatal(err): テストを失敗としてマークし、現在のテスト関数を直ちに終了します。t.Skip(reason): テストをスキップします。これは、特定の環境でのみ実行されるべきテストや、まだ修正されていないバグをテストする際に便利です。このコミットでは、まさにこのt.Skipが使用されています。
プリペアドステートメントとリソース管理
データベース操作において、プリペアドステートメント(sql.Stmt)は、SQLクエリを事前にデータベースに送信してコンパイルさせることで、繰り返し実行時のオーバーヘッドを削減し、SQLインジェクション攻撃を防ぐ重要な役割を果たします。
database/sqlパッケージでは、StmtやRowsのようなオブジェクトは、内部的にデータベース接続上のリソース(ステートメントハンドル、カーソルなど)を保持しています。これらのリソースは、明示的にClose()メソッドを呼び出すか、またはRowsの場合は結果セットの最後まで読み込むことで解放されるべきです。リソースの適切な管理は、データベース接続の枯渇やデッドロックを防ぐために不可欠です。
このコミットで追加されたテストは、このリソース管理、特にStmtとRowsのライフサイクルが交錯する際の潜在的な問題に焦点を当てています。
技術的詳細
このコミットは、src/pkg/database/sql/sql_test.goファイルにTestCloseStmtBeforeRowsという新しいテスト関数を追加しています。このテストの目的は、sql.Rowsオブジェクトがまだ開いている(つまり、結果セットが完全に読み取られていない)状態で、そのRowsを生成したsql.Stmtオブジェクトをクローズしようとした場合に何が起こるかを検証することです。
テストのロジックは以下の通りです。
newTestDBでテスト用のデータベース接続を確立します。db.Prepare("SELECT|people|name|")でプリペアドステートメントsを作成します。このSQLはテスト用のダミーであり、実際のデータベースに依存しないように設計されています。s.Query()を呼び出して結果セットr(sql.Rows)を取得します。この時点で、Rowsオブジェクトは開いており、データベースからの結果を読み取る準備ができています。- 重要な点:
rがまだ開いているにもかかわらず、s.Close()を呼び出してStmtをクローズしようとします。 - 最後に
r.Close()を呼び出してRowsをクローズします。
このテストは、t.Skip("known broken test; golang.org/issue/3865")という行で始まっています。これは、このテストが意図的にスキップされることを意味します。つまり、このテストは現在の実装では失敗することが分かっており、golang.org/issue/3865で追跡されているバグが修正されるまで実行されないように設定されています。
このテストが失敗するということは、sql.Rowsがまだアクティブな状態でsql.Stmtをクローズしようとすると、database/sqlパッケージの内部で何らかの問題(例えば、デッドロック、リソースリーク、または予期せぬエラー)が発生することを示唆しています。理想的には、Stmt.Close()はRowsが完全に処理されるまでブロックするか、あるいはRowsがクローズされた後にのみStmtのリソースを解放するべきです。
コアとなるコードの変更箇所
変更はsrc/pkg/database/sql/sql_test.goファイルに集中しています。
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -448,6 +448,32 @@ func TestIssue2542Deadlock(t *testing.T) {
}\n}\n \n+func TestCloseStmtBeforeRows(t *testing.T) {\n+\tt.Skip(\"known broken test; golang.org/issue/3865\")\n+\treturn\n+\n+\tdb := newTestDB(t, \"people\")\n+\tdefer closeDB(t, db)\n+\n+\ts, err := db.Prepare(\"SELECT|people|name|\")\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\n+\tr, err := s.Query()\n+\tif err != nil {\n+\t\ts.Close()\n+\t\tt.Fatal(err)\n+\t}\n+\n+\terr = s.Close()\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\n+\tr.Close()\n+}\n+\n // Tests fix for issue 2788, that we bind nil to a []byte if the\n // value in the column is sql null\n func TestNullByteSlice(t *testing.T) {
コアとなるコードの解説
追加されたTestCloseStmtBeforeRows関数は、database/sqlパッケージのStmtとRowsのライフサイクルにおける潜在的な問題を浮き彫りにするためのものです。
func TestCloseStmtBeforeRows(t *testing.T) {
t.Skip("known broken test; golang.org/issue/3865") // テストをスキップ
return // スキップされた場合はここで関数を終了
db := newTestDB(t, "people") // テスト用DB接続の取得
defer closeDB(t, db) // 関数終了時にDB接続をクローズ
s, err := db.Prepare("SELECT|people|name|") // プリペアドステートメントの準備
if err != nil {
t.Fatal(err) // エラーがあればテスト失敗
}
r, err := s.Query() // クエリを実行し、結果セット(Rows)を取得
if err != nil {
s.Close() // Queryでエラーが発生した場合、Stmtをクローズ
t.Fatal(err) // エラーがあればテスト失敗
}
err = s.Close() // ★問題の箇所: Rowsがまだ開いている状態でStmtをクローズ
if err != nil {
t.Fatal(err) // エラーがあればテスト失敗
}
r.Close() // Rowsをクローズ
}
このテストの核心は、s.Close()がr.Close()の前に呼び出されている点です。database/sqlの設計では、Rowsオブジェクトがデータベース接続上のカーソルや結果セットのリソースを保持しており、これらのリソースはRows.Close()が呼び出されるか、またはすべての行が読み取られたときに解放されることが期待されます。
もしStmt.Close()がRowsがまだアクティブな状態で呼び出された場合、以下のような問題が発生する可能性があります。
- デッドロック:
Stmt.Close()がRowsが保持しているリソースの解放を待つが、RowsがStmtがクローズされるのを待つ、といった循環的な依存関係が発生し、デッドロックに陥る。 - リソースリーク:
Stmtがクローズされても、Rowsが保持しているリソースが適切に解放されず、データベース接続上のリソースが枯渇する。 - 予期せぬエラー:
Stmtがクローズされた後にRowsが操作されようとすると、無効なハンドルや接続エラーが発生する。
このテストは、これらの問題のいずれかが実際に発生することを確認するためのものであり、t.Skipによって、この問題がまだ解決されていない「既知のバグ」であることを明示しています。このテストが将来的に有効化され、パスするようになれば、golang.org/issue/3865のバグが修正されたことを意味します。
関連リンク
- Go Issue 3865: https://golang.org/issue/3865
- Go Code Review 7324051: https://golang.org/cl/7324051
参考にした情報源リンク
- Go Issue 3865: https://golang.org/issue/3865
- Go Code Review 7324051: https://golang.org/cl/7324051
- Go言語
database/sqlパッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/database/sql - Go言語
testingパッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/testing - Go言語におけるデータベースプログラミングの基本 (一般的なGoのデータベースに関する記事やチュートリアル)
- プリペアドステートメントに関する一般的なデータベースの概念 (SQLインジェクション対策など)