[インデックス 16848] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおけるリソース管理の改善を目的としています。具体的には、データベース接続が解放される前に、関連するステートメント(プリペアドステートメント)が確実にクローズされるように修正が加えられています。これにより、潜在的なリソースリークや、ステートメントが適切にクローズされないことによる問題が回避されます。
コミット
commit a293065a39963ef2efb4cc133e286d2d97b356a9
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Jul 23 14:09:53 2013 +1000
database/sql: close statement before connection
Fixes #5936
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/11620046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a293065a39963ef2efb4cc133e286d2d97b356a9
元コミット内容
このコミットの元の内容は以下の通りです。
database/sql: close statement before connection
Fixes #5936
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/11620046
これは、database/sql
パッケージにおいて、データベース接続を解放する前にステートメントをクローズするように修正するというものです。Fixes #5936
とあることから、GoのIssueトラッカーで報告された問題 #5936 を修正する目的で行われたことがわかります。
変更の背景
この変更の背景には、database/sql
パッケージにおけるエラーハンドリングとリソース管理の課題がありました。queryConn
関数内でエラーが発生した場合、以前の実装では releaseConn(err)
が si.Close()
の前に呼び出されていました。releaseConn
はデータベース接続をプールに戻す、あるいはクローズする役割を担いますが、この時点で si
(ステートメントインターフェース) がまだクローズされていないと、ステートメントに関連するリソースが適切に解放されない可能性がありました。
特に、データベースドライバによっては、ステートメントがクローズされる前に接続が解放されると、そのステートメントが「ぶら下がった」状態になり、データベースサーバー側でリソースリークを引き起こしたり、後続の操作で予期せぬエラーが発生したりするリスクがありました。このコミットは、このような潜在的なリソースリークや不安定性を解消するために、ステートメントのクローズ処理を接続の解放よりも先行させるように修正しています。
Fixes #5936
とあることから、この問題はユーザーによって報告され、具体的な不具合として認識されていたと考えられます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の database/sql
パッケージに関する基本的な知識が必要です。
database/sql
パッケージ: Go言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作はドライバによって実装されます。driver.Conn
: データベースへの単一の接続を表すインターフェースです。driver.Stmt
: プリペアドステートメントを表すインターフェースです。SQLクエリを事前に準備し、繰り返し実行する際に使用されます。これにより、クエリのパースオーバーヘッドを削減し、SQLインジェクション攻撃を防ぐことができます。driver.Rows
: クエリ結果の行を表すインターフェースです。DB
構造体:database/sql
パッケージの主要なエントリポイントであり、データベースへの接続プールを管理します。- 接続プール: データベースへの接続を再利用するための仕組みです。新しい接続を確立するコストを削減し、アプリケーションのパフォーマンスを向上させます。接続が不要になった場合、すぐにクローズするのではなく、プールに戻して再利用できるようにします。
- リソース管理: データベース接続やステートメントなどのリソースは、使用後に適切に解放される必要があります。解放されない場合、リソースリークが発生し、アプリケーションやデータベースサーバーのパフォーマンスに悪影響を与える可能性があります。
defer
ステートメント: Go言語のdefer
ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールします。リソースの解放(ファイルクローズ、ロック解除など)によく使用されます。ただし、このコミットの変更箇所ではdefer
は使用されていません。
技術的詳細
このコミットの技術的な核心は、database/sql
パッケージ内の queryConn
関数におけるエラーパスの処理順序の変更です。
queryConn
関数は、データベース接続 (dc
) を使用してクエリを実行し、結果の行 (rowsi
) を返す役割を担っています。この関数内で、プリペアドステートメント (si
) が作成され、それを使用してクエリが実行されます。
変更前のコードでは、クエリの実行中にエラー (err != nil
) が発生した場合、以下の順序で処理が行われていました。
releaseConn(err)
: データベース接続を解放(プールに戻すかクローズする)dc.Lock()
: 接続のロックsi.Close()
: ステートメントをクローズdc.Unlock()
: 接続のアンロック
この順序の問題点は、releaseConn(err)
が si.Close()
の前に呼び出されている点です。もし releaseConn
が接続をプールに戻したり、物理的にクローズしたりした場合、その接続に関連付けられた si
(ステートメント) はまだクローズされていませんでした。これにより、ドライバによっては、ステートメントが適切にクローズされずにリソースがリークしたり、後続の操作で問題が発生したりする可能性がありました。
変更後のコードでは、この順序が以下のように修正されました。
dc.Lock()
: 接続のロックsi.Close()
: ステートメントをクローズdc.Unlock()
: 接続のアンロックreleaseConn(err)
: データベース接続を解放
この変更により、エラーが発生した場合でも、データベース接続が解放される前に、必ず関連するステートメントがクローズされることが保証されます。これにより、ステートメントに関連するデータベースサーバー側のリソースが確実に解放され、リソースリークや不安定性の問題が解消されます。
また、このコミットには TestStmtCloseOrder
という新しいテストケースが追加されています。このテストは、存在しないテーブルに対してクエリを実行することで意図的にエラーを発生させ、その際にステートメントが適切にクローズされることを検証しています。SetMaxIdleConns(0)
と setStrictFakeConnClose(t)
を使用して、接続がすぐにクローズされるような厳密な環境を設定し、ステートメントのクローズ順序が正しく機能するかを確認しています。
コアとなるコードの変更箇所
変更は src/pkg/database/sql/sql.go
ファイルの queryConn
関数内で行われています。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -759,10 +759,10 @@ func (db *DB) queryConn(dc *driverConn, releaseConn func(error), query string, a
ds := driverStmt{dc, si}
rowsi, err := rowsiFromStatement(ds, args...)
if err != nil {
- releaseConn(err)
dc.Lock()
si.Close()
dc.Unlock()
+ releaseConn(err)
return nil, err
}
また、テストファイル src/pkg/database/sql/sql_test.go
に新しいテストケース TestStmtCloseOrder
が追加されています。
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1046,6 +1046,20 @@ func TestRowsCloseOrder(t *testing.T) {
}
}
+func TestStmtCloseOrder(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+
+ db.SetMaxIdleConns(0)
+ setStrictFakeConnClose(t)
+ defer setStrictFakeConnClose(nil)
+
+ _, err := db.Query("SELECT|non_existent|name|")
+ if err == nil {
+ t.Fatal("Quering non-existent table should fail")
+ }
+}
+
func manyConcurrentQueries(t testOrBench) {
maxProcs, numReqs := 16, 500
if testing.Short() {
コアとなるコードの解説
sql.go
の変更点では、if err != nil
ブロック内で、releaseConn(err)
の呼び出しが dc.Lock()
と si.Close()
のブロックの後に移動されています。
-
変更前:
if err != nil { releaseConn(err) // 接続を解放 dc.Lock() si.Close() // ステートメントをクローズ dc.Unlock() return nil, err }
この順序では、
releaseConn
が呼び出された時点でsi
がまだクローズされていないため、接続が解放された後にsi
をクローズしようとすると、ドライバによっては問題が発生する可能性がありました。 -
変更後:
if err != nil { dc.Lock() si.Close() // ステートメントをクローズ dc.Unlock() releaseConn(err) // 接続を解放 return nil, err }
この新しい順序では、エラーが発生した場合でも、まず
dc.Lock()
で接続をロックし、si.Close()
でステートメントを確実にクローズしてから、dc.Unlock()
でロックを解除し、最後にreleaseConn(err)
で接続を解放します。これにより、ステートメントに関連するリソースが、接続がプールに戻されるかクローズされる前に確実に解放されることが保証されます。
sql_test.go
に追加された TestStmtCloseOrder
は、この修正が正しく機能することを検証するためのものです。
db := newTestDB(t, "people")
: テスト用のデータベース接続を作成します。db.SetMaxIdleConns(0)
: アイドル接続の最大数を0に設定します。これにより、クエリ実行後に接続がすぐにクローズされるようになり、リソース解放のタイミングが厳密になります。setStrictFakeConnClose(t)
: これはテストヘルパー関数で、おそらく接続がクローズされる際に特定の検証を行うように設定します。_, err := db.Query("SELECT|non_existent|name|")
: 意図的に存在しないテーブルに対してクエリを実行し、エラーを発生させます。if err == nil { t.Fatal("Quering non-existent table should fail") }
: エラーが発生しない場合はテストを失敗させます。
このテストは、エラーパスにおいてステートメントが適切にクローズされることを保証するための重要な検証ステップです。
関連リンク
- Go言語
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go言語のIssueトラッカー (このコミットが修正したIssue #5936): 検索結果では関連するIssueが見つかりませんでしたが、GoのIssueトラッカーは https://github.com/golang/go/issues にあります。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のGitHubリポジトリのコミット履歴
- Go言語のIssueトラッカー (ただし、#5936 は直接的な情報が見つからず、一般的なGoのIssue検索結果を参照しました)