[インデックス 12554] ファイルの概要
このコミットは、Go言語の標準ライブラリであるdatabase/sql
パッケージにおいて、ステートメント(Stmt
)および行セット(Rows
)のリソースリークを防ぐための修正を導入しています。特に、テスト用のfakedb
ドライバーに未クローズのStmt
を検出するメカニズムを追加し、それに基づいて既存のコードベースにおけるリソース解放の漏れを修正しています。また、Tx.Query
の挙動をDB.Query
に合わせるための調整も含まれています。
コミット
commit c3954dd5da820306dff6bbd73a7801d9739b038f
Author: Gwenael Treguier <gwenn.kahz@gmail.com>
Date: Sat Mar 10 15:21:44 2012 -0800
database/sql: ensure Stmts are correctly closed.
To make sure that there is no resource leak,
I suggest to fix the 'fakedb' driver such as it fails when any
Stmt is not closed.
First, add a check in fakeConn.Close().
Then, fix all missing Stmt.Close()/Rows.Close().
I am not sure that the strategy choose in fakeConn.Prepare/prepare* is ok.
The weak point in this patch is the change in Tx.Query:
- Tests pass without this change,
- I found it by manually analyzing the code,
- I just try to make Tx.Query look like DB.Query.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5759050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c3954dd5da820306dff6bbd73a7801d9739b038f
元コミット内容
database/sql: ensure Stmts are correctly closed.
To make sure that there is no resource leak,
I suggest to fix the 'fakedb' driver such as it fails when any
Stmt is not closed.
First, add a check in fakeConn.Close().
Then, fix all missing Stmt.Close()/Rows.Close().
I am not sure that the strategy choose in fakeConn.Prepare/prepare* is ok.
The weak point in this patch is the change in Tx.Query:
- Tests pass without this change,
- I found it by manually analyzing the code,
- I just try to make Tx.Query look like DB.Query.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5759050
変更の背景
このコミットの主な背景は、database/sql
パッケージにおけるリソースリークの可能性を排除することです。データベース操作において、プリペアドステートメント(Stmt
)やクエリ結果セット(Rows
)は、使用後に適切にクローズされないと、データベース接続やメモリなどのシステムリソースを消費し続け、アプリケーションのパフォーマンス低下や最終的なリソース枯渇を引き起こす可能性があります。
特に、このコミットでは以下の点が問題視されていました。
- テスト環境でのリーク検出の困難さ: 実際のデータベース接続ではリソースリークが発生しても、テスト環境(特に
fakedb
のようなモックドライバー)ではその問題が顕在化しにくいという課題がありました。そのため、fakedb
にリーク検出機能を追加することで、開発段階で問題を早期に発見できるようにする必要がありました。 - 既存コードにおけるリソース解放の漏れ:
fakedb
にリーク検出機能を追加した結果、既存のテストコードやdatabase/sql
パッケージ内部のコードで、Stmt
やRows
が適切にクローズされていない箇所が発見されました。これらの箇所を修正し、リソースが確実に解放されるようにする必要がありました。 Tx.Query
の挙動の一貫性:Tx.Query
(トランザクション内のクエリ)の挙動が、DB.Query
(データベース接続全体のクエリ)と異なり、エラー発生時にStmt
がクローズされないという不整合がありました。これにより、エラーパスでのリソースリークの可能性が生じていたため、両者の挙動を統一し、堅牢性を高める必要がありました。
これらの背景から、リソース管理の厳密化とコードの堅牢性向上を目的とした修正が導入されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語のdatabase/sql
パッケージに関する知識が不可欠です。
database/sql
パッケージ
Go言語のdatabase/sql
パッケージは、SQLデータベースへの汎用的なインターフェースを提供します。これにより、特定のデータベースドライバーに依存しない形でデータベース操作を行うことができます。このパッケージは、データベースとの接続プール管理、プリペアドステートメントのキャッシュ、トランザクション管理などの機能を提供します。
driver
インターフェース
database/sql
パッケージは、driver
インターフェースを介して実際のデータベースドライバーと連携します。各データベース(例: MySQL, PostgreSQL, SQLite)は、このdriver
インターフェースを実装することで、database/sql
パッケージから利用可能になります。
DB
型
DB
型は、データベースへの接続プールを表します。通常、アプリケーションの起動時に一度だけ作成され、複数のゴルーチンから安全に利用されます。
Tx
型
Tx
型は、データベーストランザクションを表します。DB.Begin()
メソッドを呼び出すことで開始され、Commit()
またはRollback()
で終了します。トランザクション内で実行される操作は、すべてアトミックに処理されます。
Stmt
型(プリペアドステートメント)
Stmt
型は、プリペアドステートメントを表します。プリペアドステートメントは、SQLクエリを事前にデータベースに送信して準備しておくことで、繰り返し実行する際のパフォーマンスを向上させ、SQLインジェクション攻撃を防ぐのに役立ちます。
DB.Prepare()
またはTx.Prepare()
で作成され、Exec()
やQuery()
メソッドで実行されます。
重要: Stmt
はデータベースリソースを消費するため、使用後は必ずStmt.Close()
メソッドを呼び出して解放する必要があります。
Rows
型(クエリ結果セット)
Rows
型は、Query()
メソッドの実行結果である行セットを表します。クエリ結果をイテレートして取得するために使用されます。
重要: Rows
もデータベースリソース(カーソルなど)を消費するため、すべての行を読み終えるか、エラーが発生した場合は、必ずRows.Close()
メソッドを呼び出して解放する必要があります。通常はdefer rows.Close()
のように記述されます。
リソースリーク
リソースリークとは、プログラムが獲得したシステムリソース(メモリ、ファイルハンドル、ネットワークソケット、データベース接続、プリペアドステートメントなど)を、使用後に適切に解放しないために、それらのリソースがシステム内に残り続けてしまう現象を指します。リソースリークが発生すると、システムのパフォーマンスが低下したり、最終的にはリソースが枯渇してアプリケーションがクラッシュしたりする原因となります。
fakedb
ドライバー
fakedb
は、database/sql
パッケージのテストのために使用されるモック(偽の)データベースドライバーです。実際のデータベースに接続することなく、データベース操作のシミュレーションを行うことで、テストの実行速度を向上させ、外部依存性を排除します。このコミットでは、このfakedb
にリソースリーク検出機能が追加されています。
技術的詳細
このコミットは、主に以下の3つの技術的側面に焦点を当てています。
-
fakedb
におけるStmt
リーク検出メカニズムの導入:src/pkg/database/sql/fakedb_test.go
内のfakeConn
構造体に、stmtsMade
(作成されたStmt
の数)とstmtsClosed
(クローズされたStmt
の数)というカウンターが追加されました。fakeConn.Prepare
メソッドが呼び出されるたびにstmtsMade
がインクリメントされ、fakeStmt.Close()
が呼び出されるたびにstmtsClosed
がインクリメントされます。fakeConn.Close()
メソッドに、stmtsMade > stmtsClosed
の場合にエラーを返すチェックが追加されました。これにより、接続がクローズされる際に未クローズのStmt
が存在するとテストが失敗するようになり、リソースリークを検出できるようになりました。
-
fakedb
のPrepare
関連メソッドにおけるエラーパスでのStmt.Close()
の追加:fakedb_test.go
内のprepareSelect
,prepareCreate
,prepareInsert
などのprepare*
ヘルパー関数や、fakeConn.Prepare
メソッドにおいて、SQL構文エラーやその他の準備段階でのエラーが発生した場合に、作成途中のStmt
インスタンスが適切にクローズされるようにstmt.Close()
が追加されました。これにより、エラーパスでもリソースが確実に解放されるようになりました。
-
Tx.Query
におけるStmt
のクローズロジックの修正:src/pkg/database/sql/sql.go
内のTx.Query
メソッドのロジックが変更されました。以前は、stmt.Query(args...)
が成功した場合にのみrows.closeStmt = stmt
が設定され、Rows
がクローズされる際にStmt
もクローズされるようになっていました。- しかし、
stmt.Query(args...)
がエラーを返した場合、stmt
はクローズされずに残ってしまう可能性がありました。 - この修正では、
stmt.Query(args...)
がエラーを返した場合に、即座にstmt.Close()
を呼び出すように変更されました。これにより、DB.Query
と同様に、Tx.Query
でもエラー発生時にStmt
が確実にクローズされるようになり、一貫性と堅牢性が向上しました。
-
sql_test.go
におけるStmt.Close()
およびRows.Close()
の追加:src/pkg/database/sql/sql_test.go
内の複数のテストケース(TestStatementQueryRow
,TestExec
,TestTxStmt
,TestTxQuery
,nullTestRun
など)において、Stmt
やRows
が使用された後にdefer stmt.Close()
やdefer r.Close()
が追加されました。これは、fakedb
のリーク検出機能によって発見された既存のリーク箇所を修正するものです。これにより、テストコード自体がリソース管理のベストプラクティスに従うようになり、より信頼性の高いテストが可能になりました。
これらの変更により、database/sql
パッケージ全体のリソース管理が強化され、特にエラー発生時やテスト環境でのリソースリークが効果的に防止されるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更箇所は以下の通りです。
src/pkg/database/sql/fakedb_test.go
-
fakeConn.Close()
でのStmt
リークチェックの追加:--- a/src/pkg/database/sql/fakedb_test.go +++ b/src/pkg/database/sql/fakedb_test.go @@ -214,6 +214,9 @@ func (c *fakeConn) Close() error { if c.db == nil { return errors.New("can't close fakeConn; already closed") } + if c.stmtsMade > c.stmtsClosed { + return errors.New("can't close; dangling statement(s)") + } c.db = nil return nil }
-
prepareSelect
などのエラーパスでのstmt.Close()
の追加:--- a/src/pkg/database/sql/fakedb_test.go +++ b/src/pkg/database/sql/fakedb_test.go @@ -250,6 +253,7 @@ func errf(msg string, args ...interface{}) error { // just a limitation for fakedb) func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 3 { + stmt.Close() return nil, errf("invalid SELECT syntax with %d parts; want 3", len(parts)) } stmt.table = parts[0] @@ -260,14 +264,17 @@ func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, e } nameVal := strings.Split(colspec, "=") if len(nameVal) != 2 { + stmt.Close() return nil, errf("SELECT on table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } column, value := nameVal[0], nameVal[1] _, ok := c.db.columnType(stmt.table, column) if !ok { + stmt.Close() return nil, errf("SELECT on table %q references non-existent column %q", stmt.table, column) } if value != "?" { + stmt.Close() return nil, errf("SELECT on table %q has pre-bound value for where column %q; need a question mark", stmt.table, column) } @@ -280,12 +287,14 @@ func (c *fakeConn) prepareCreate(stmt *fakeStmt, parts []string) (driver.Stmt, e // parts are table|col=type,col2=type2 func (c *fakeConn) prepareCreate(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 2 { + stmt.Close() return nil, errf("invalid CREATE syntax with %d parts; want 2", len(parts)) } stmt.table = parts[0] for n, colspec := range strings.Split(parts[1], ",") { nameType := strings.Split(colspec, "=") if len(nameType) != 2 { + stmt.Close() return nil, errf("CREATE table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } stmt.colName = append(stmt.colName, nameType[0]) @@ -297,17 +306,20 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e // parts are table|col=?,col2=val func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 2 { + stmt.Close() return nil, errf("invalid INSERT syntax with %d parts; want 2", len(parts)) } stmt.table = parts[0] for n, colspec := range strings.Split(parts[1], ",") { nameVal := strings.Split(colspec, "=") if len(nameVal) != 2 { + stmt.Close() return nil, errf("INSERT table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } column, value := nameVal[0], nameVal[1] ctype, ok := c.db.columnType(stmt.table, column) if !ok { + stmt.Close() return nil, errf("INSERT table %q references non-existent column %q", stmt.table, column) } stmt.colName = append(stmt.colName, column) @@ -323,10 +335,12 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e case "int32": i, err := strconv.Atoi(value) if err != nil { + stmt.Close() return nil, errf("invalid conversion to int32 from %q", value) } subsetVal = int64(i) // int64 is a subset type, but not int32 default: + stmt.Close() return nil, errf("unsupported conversion for pre-bound parameter %q to type %q", value, ctype) } stmt.colValue = append(stmt.colValue, subsetVal) @@ -362,6 +376,7 @@ func (c *fakeConn) Prepare(query string) (driver.Stmt, error) { case "INSERT": return c.prepareInsert(stmt, parts) default: + stmt.Close() return nil, errf("unsupported command type %q", cmd) } return stmt, nil
src/pkg/database/sql/sql.go
Tx.Query
でのエラー時のStmt
クローズロジックの修正:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -612,9 +612,11 @@ func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) { return nil, err }\n rows, err := stmt.Query(args...)\n -\tif err == nil {\n -\t\trows.closeStmt = stmt\n +\tif err != nil {\n +\t\tstmt.Close()\n +\t\treturn nil, err\n }\n +\trows.closeStmt = stmt\n return rows, err }\n @@ -1020,7 +1022,7 @@ func (r *Row) Scan(dest ...interface{}) error { }\n // TODO(bradfitz): for now we need to defensively clone all\n -\t// []byte that the driver returned (not permitting \n +\t// []byte that the driver returned (not permitting\n // *RawBytes in Rows.Scan), since we're about to close\n // the Rows in our defer, when we return from this function.\n // the contract with the driver.Next(...) interface is that it
src/pkg/database/sql/sql_test.go
- 既存テストケースでの
defer stmt.Close()
およびdefer r.Close()
の追加:--- a/src/pkg/database/sql/sql_test.go +++ b/src/pkg/database/sql/sql_test.go @@ -251,6 +251,7 @@ func TestStatementQueryRow(t *testing.T) { if err != nil { t.Fatalf("Prepare: %v", err) } + defer stmt.Close() var age int for n, tt := range []struct { name string @@ -291,6 +292,7 @@ func TestExec(t *testing.T) { if err != nil { t.Errorf("Stmt, err = %v, %v", stmt, err) } + defer stmt.Close() type execTest struct { args []interface{} @@ -332,11 +334,14 @@ func TestTxStmt(t *testing.T) { if err != nil { t.Fatalf("Stmt, err = %v, %v", stmt, err) } + defer stmt.Close() tx, err := db.Begin() if err != nil { t.Fatalf("Begin = %v", err) } - _, err = tx.Stmt(stmt).Exec("Bobby", 7) + txs := tx.Stmt(stmt) + defer txs.Close() + _, err = txs.Exec("Bobby", 7) if err != nil { t.Fatalf("Exec = %v", err) } @@ -365,6 +370,7 @@ func TestTxQuery(t *testing.T) { if err != nil { t.Fatal(err) } + defer r.Close() if !r.Next() { if r.Err() != nil { @@ -561,6 +567,7 @@ func nullTestRun(t *testing.T, spec nullTestSpec) { if err != nil { t.Fatalf("prepare: %v", err) } + defer stmt.Close() if _, err := stmt.Exec(3, "chris", spec.rows[2].nullParam, spec.rows[2].notNullParam); err != nil { t.Errorf("exec insert chris: %v", err) }
コアとなるコードの解説
fakedb_test.go
の変更
-
fakeConn.Close()
でのリークチェック:fakeConn
はテスト用の偽のデータベース接続を表します。この変更により、fakeConn
がクローズされる際に、作成されたStmt
の数(stmtsMade
)とクローズされたStmt
の数(stmtsClosed
)を比較し、未クローズのStmt
が存在すればエラーを発生させるようになりました。これは、fakedb
を利用したテストにおいて、Stmt
のリソースリークを自動的に検出するための重要なメカニズムです。これにより、開発者はテストを実行するだけで、Stmt.Close()
の呼び出し忘れを特定できるようになります。 -
prepare*
関数およびfakeConn.Prepare
でのstmt.Close()
:fakedb
のPrepare
関連の関数(prepareSelect
,prepareCreate
,prepareInsert
など)は、SQLクエリの解析や準備を行う際に、様々なエラーチェックを行います。これらのチェックでエラーが検出された場合、Stmt
オブジェクトは完全に初期化されず、defer stmt.Close()
のような通常のクリーンアップメカニズムが適用されない可能性があります。この変更は、エラーが発生した直後にstmt.Close()
を明示的に呼び出すことで、部分的に作成されたStmt
オブジェクトがリソースをリークするのを防ぎます。これは、エラーパスにおけるリソース管理の堅牢性を高めるための重要な修正です。
sql.go
の変更
Tx.Query
でのエラー時のStmt
クローズロジック:Tx.Query
メソッドは、トランザクション内でクエリを実行し、Rows
オブジェクトを返します。以前の実装では、stmt.Query(args...)
が成功した場合にのみ、返されたRows
オブジェクトにstmt
が関連付けられ、Rows
がクローズされる際にstmt
もクローズされるようになっていました。しかし、stmt.Query(args...)
自体がエラーを返した場合、stmt
はRows
に関連付けられず、結果としてクローズされないまま残ってしまう可能性がありました。 この修正では、stmt.Query(args...)
がエラーを返した場合に、即座にstmt.Close()
を呼び出すように変更されました。これにより、Tx.Query
がエラーを返して終了する際にも、Stmt
リソースが確実に解放されるようになり、DB.Query
の挙動との一貫性が保たれ、リソースリークのリスクが低減されました。
sql_test.go
の変更
- 既存テストケースでの
defer stmt.Close()
およびdefer r.Close()
の追加: これらの変更は、fakedb
に導入されたリーク検出機能によって発見された、既存のテストコードにおけるリソース解放の漏れを修正するものです。defer
キーワードを使用することで、関数が終了する際にstmt.Close()
やr.Close()
が確実に呼び出されるようになります。これはGo言語におけるリソース管理のベストプラクティスであり、テストコード自体がリソースリークの原因とならないようにするために重要です。これにより、テストの信頼性が向上し、将来的なリソースリークの混入を防ぐための良い例となります。
これらの変更は全体として、database/sql
パッケージのリソース管理をより厳密にし、特にエラー発生時やテスト環境でのリソースリークを防ぐことで、パッケージの堅牢性と信頼性を大幅に向上させています。
関連リンク
- Go言語
database/sql
パッケージ公式ドキュメント: https://pkg.go.dev/database/sql - Go言語
database/sql/driver
パッケージ公式ドキュメント: https://pkg.go.dev/database/sql/driver - A Tour of Go - Defer: https://go.dev/tour/flowcontrol/12 (Goの
defer
ステートメントに関する解説)
参考にした情報源リンク
- Go言語の
database/sql
パッケージに関する一般的な情報源やチュートリアル。 - Go言語におけるリソース管理、特に
defer
ステートメントの利用に関するベストプラクティス。 - Go言語のテストにおけるモックとスタブの利用に関する情報。
- Go言語の
database/sql
パッケージのソースコード(このコミットの変更点を含む)。 - Go言語のコードレビュープロセスと、
golang.org/cl
(Gerrit Code Review)の利用方法。 - Go言語の
database/sql
パッケージの設計思想に関する議論やドキュメント。 - Go言語の
database/sql
パッケージにおけるトランザクション管理に関する情報。 - Go言語におけるエラーハンドリングのパターン。
- Go言語の
database/sql
パッケージの歴史的な変更ログや関連するIssue。 - Go言語の
database/sql
パッケージのStmt
とRows
のライフサイクルに関する詳細な解説。