[インデックス 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インジェクション対策など)