Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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) が発生した場合、以下の順序で処理が行われていました。

  1. releaseConn(err): データベース接続を解放(プールに戻すかクローズする)
  2. dc.Lock(): 接続のロック
  3. si.Close(): ステートメントをクローズ
  4. dc.Unlock(): 接続のアンロック

この順序の問題点は、releaseConn(err)si.Close() の前に呼び出されている点です。もし releaseConn が接続をプールに戻したり、物理的にクローズしたりした場合、その接続に関連付けられた si (ステートメント) はまだクローズされていませんでした。これにより、ドライバによっては、ステートメントが適切にクローズされずにリソースがリークしたり、後続の操作で問題が発生したりする可能性がありました。

変更後のコードでは、この順序が以下のように修正されました。

  1. dc.Lock(): 接続のロック
  2. si.Close(): ステートメントをクローズ
  3. dc.Unlock(): 接続のアンロック
  4. 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 は、この修正が正しく機能することを検証するためのものです。

  1. db := newTestDB(t, "people"): テスト用のデータベース接続を作成します。
  2. db.SetMaxIdleConns(0): アイドル接続の最大数を0に設定します。これにより、クエリ実行後に接続がすぐにクローズされるようになり、リソース解放のタイミングが厳密になります。
  3. setStrictFakeConnClose(t): これはテストヘルパー関数で、おそらく接続がクローズされる際に特定の検証を行うように設定します。
  4. _, err := db.Query("SELECT|non_existent|name|"): 意図的に存在しないテーブルに対してクエリを実行し、エラーを発生させます。
  5. 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検索結果を参照しました)