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

[インデックス 12551] ファイルの概要

このコミットは、Go言語の database/sql パッケージにおける重要なバグ修正と改善を目的としています。具体的には、トランザクション内で Stmt.Query がエラーを返した場合に、データベース接続がフリーリストに二重に解放される可能性があった問題を修正しています。また、driver.ErrBadConn の伝播を改善し、問題のある接続が適切にプールから除外されるようにしています。

コミット

commit 3297fc63d6226f6ed47a4fdb5962c78c55c5339c
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sat Mar 10 10:00:02 2012 -0800

    database/sql: fix double connection free on Stmt.Query error
    
    In a transaction, on a Stmt.Query error, it was possible for a
    connection to be added to a db's freelist twice. Should use
    the local releaseConn function instead.
    
    Thanks to Gwenael Treguier for the failing test.
    
    Also in this CL: propagate driver errors through releaseConn
    into *DB.putConn, which conditionally ignores the freelist
    addition if the driver signaled ErrBadConn, introduced in a
    previous CL.
    
    R=golang-dev, gary.burd
    CC=golang-dev
    https://golang.org/cl/5798049

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3297fc63d6226f6ed47a4fdb5962c78c55c5339c

元コミット内容

トランザクション内で Stmt.Query がエラーを返した場合に、データベース接続が db のフリーリストに二重に解放される可能性があった問題を修正します。この修正では、ローカルの releaseConn 関数を適切に使用するように変更されています。また、releaseConn を介してドライバエラーを *DB.putConn に伝播させ、driver.ErrBadConn がシグナルされた場合にはフリーリストへの追加を条件付きで無視するように改善されています。

変更の背景

この変更は、database/sql パッケージにおける接続管理の堅牢性を高めるために行われました。

  1. 二重解放のバグ: 以前の実装では、トランザクション内で Stmt.Query メソッドがエラーを返した場合、内部的に接続が二重にフリーリスト(接続プール)に戻される可能性がありました。これは、接続プールの状態を不正にし、後続のデータベース操作で予期せぬ動作やパニックを引き起こす可能性のある深刻なバグです。接続が二重に解放されると、同じ接続が複数のゴルーチンに割り当てられたり、接続が閉じられた後に再度使用されたりする「Use-After-Free」のような問題につながる可能性があります。

  2. driver.ErrBadConn の伝播: database/sql パッケージは、データベースドライバが driver.ErrBadConn を返すことで、接続が不良状態であることを通知するメカニズムを持っています。以前のコミットでこのエラーが導入されましたが、このコミットでは、Stmt.Query のエラーパスにおいてもこの ErrBadConn が適切に putConn 関数に伝播されるように改善されています。これにより、不良な接続が接続プールに再利用可能なものとして戻されることを防ぎ、アプリケーションが常に健全な接続を使用できるようにします。

これらの問題は、データベースとのインタラクションにおいて信頼性と安定性を確保するために、早急に修正する必要がありました。特に、トランザクションは複数の操作をアトミックに実行する性質上、その途中で発生するエラーのハンドリングは非常に重要です。

前提知識の解説

このコミットを理解するためには、Go言語の database/sql パッケージの基本的な概念と、データベース接続管理に関する知識が必要です。

  1. database/sql パッケージ: Go標準ライブラリの一部で、GoアプリケーションからSQLデータベースと対話するための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、driver インターフェースを実装するサードパーティのドライバを介してデータベースと通信します。

  2. 接続プール (Connection Pool): database/sql パッケージは、データベース接続のプールを内部的に管理します。アプリケーションがデータベース操作を行う際、新しい接続を毎回確立するのではなく、プールから既存の接続を再利用します。これにより、接続確立のオーバーヘッドを削減し、パフォーマンスを向上させます。操作が完了すると、接続はプールに戻され、他の操作のために再利用可能になります。

  3. *sql.DB: データベースへの抽象的なハンドルを表します。これは接続プールを管理し、新しい接続の取得や既存の接続の解放を行います。

  4. driver.Conn: データベースドライバが実装するインターフェースで、単一のデータベース接続を表します。

  5. *sql.Stmt (Prepared Statement): プリペアドステートメントを表します。これは、SQLクエリを事前にデータベースに送信して準備しておくことで、繰り返し実行する際のパフォーマンスを向上させます。トランザクション内で作成された Stmt は、そのトランザクションに紐付けられます。

  6. Query メソッド: *sql.DB*sql.Tx*sql.Stmt などで提供されるメソッドで、結果セット(行の集合)を返すSQLクエリを実行するために使用されます。

  7. *sql.Tx (Transaction): データベーストランザクションを表します。トランザクションは、複数のデータベース操作を単一の論理的な作業単位としてグループ化し、すべて成功するか、すべて失敗する(ロールバックされる)ことを保証します。トランザクション内で取得された接続は、そのトランザクションがコミットまたはロールバックされるまで解放されません。

  8. releaseConn 関数: このコミットで修正の対象となっている、接続を解放し、接続プールに戻すための内部的なヘルパー関数です。

  9. putConn 関数: *sql.DB の内部メソッドで、使用済みの driver.Conn を接続プール(フリーリスト)に戻す役割を担います。

  10. driver.ErrBadConn: database/sql/driver パッケージで定義されている特別なエラーです。データベースドライバが、その接続が不良状態であり、再利用できないことを database/sql パッケージに通知するために使用します。例えば、ネットワークの問題で接続が切断された場合や、データベースサーバーが接続を閉じた場合などにドライバがこのエラーを返します。database/sql パッケージは ErrBadConn を受け取ると、その接続をプールから破棄し、新しい接続を確立して操作を再試行しようとします。これにより、アプリケーションは不良な接続を意識することなく、透過的に健全な接続を利用できます。

技術的詳細

このコミットの技術的詳細は、主に database/sql パッケージの内部的な接続管理ロジック、特にトランザクションとプリペアドステートメントのエラーハンドリングに焦点を当てています。

1. 二重解放の修正: 問題は、トランザクション内で Stmt.Query がエラーを返した際に、接続が二重に db.freeConn に追加される可能性があった点です。 元のコードでは、Stmt.Query のエラーパスで s.db.putConn(ci, err) が直接呼び出されていました。しかし、Stmt がトランザクションに紐付けられている場合、接続の解放は s.tx.releaseConn() を通じて行われるべきでした。 このコミットでは、Stmt.Query のエラーパスで releaseConn(err) を呼び出すように変更されています。ここでいう releaseConn は、Stmt.connStmt() 関数内で定義されるクロージャであり、Stmt がトランザクションに紐付けられているか否かに応じて、適切な接続解放ロジック(s.tx.releaseConn() または s.db.putConn(conn, nil))を呼び出すように設計されています。これにより、トランザクション内の Stmt.Query エラー時にも、接続が一度だけ、かつ適切な方法で解放されることが保証されます。

2. driver.ErrBadConn の伝播と処理: 以前のコミットで driver.ErrBadConn が導入されましたが、このコミットではそのエラーが putConn 関数に適切に伝播されるように改善されています。 releaseConn 関数のシグネチャが func() から func(error) に変更され、エラー情報を引数として受け取るようになりました。これにより、Stmt.QueryRows.Close などで発生したエラー(特に driver.ErrBadConn)が releaseConn を経由して putConn に渡されるようになります。 *DB.putConn 関数は、渡されたエラーが driver.ErrBadConn であるかどうかをチェックします。もし ErrBadConn であれば、その接続は不良であると判断され、接続プール(db.freeConn)には追加されずに破棄されます。これにより、不良な接続がプールに残り、後続の操作で再利用されてしまうことを防ぎます。

3. テストの追加と putConnHook: このコミットには、二重解放のバグを再現し、修正を検証するための新しいテストケース (TestTxQueryInvalid) が含まれています。 また、putConnHook というテスト用のフックが追加されています。これは、putConn が呼び出された際にカスタムロジックを実行できるようにするためのもので、接続が二重に解放されていないかを検出するために使用されます。init 関数内で putConnHook が設定され、db.freeConn 内に既に存在する接続が再度 putConn に渡された場合にパニックを発生させることで、二重解放を厳密にチェックしています。

4. fakedb_test.go の変更: fakeConn.Close() メソッドのエラーメッセージがより具体的になるように修正されています。これは直接的なバグ修正ではありませんが、テストのデバッグ可能性を向上させるための改善です。

これらの変更により、database/sql パッケージの接続管理ロジックはより堅牢になり、特にエラー発生時の接続のライフサイクルが正確に管理されるようになりました。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルとコードブロックは以下の通りです。

  1. src/pkg/database/sql/fakedb_test.go:

    • fakeConn.Close() メソッド内のエラーメッセージが "can't close; in a Transaction" から "can't close fakeConn; in a Transaction" へ、また "can't close; already closed" から "can't close fakeConn; already closed" へと変更されました。これはテストのデバッグを容易にするための微調整です。
  2. src/pkg/database/sql/sql.go:

    • putConnHook という func(*DB, driver.Conn) 型のグローバル変数が追加されました。これはテスト目的で putConn の呼び出しをフックするために使用されます。
    • putConn 関数内で putConnHook が設定されている場合に呼び出されるロジックが追加されました。
    • Stmt.Exec メソッドの defer releaseConn()defer releaseConn(nil) に変更されました。これは releaseConn のシグネチャ変更に伴うものです。
    • Stmt.connStmt 関数のシグネチャが func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, err error) から func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(error), si driver.Stmt, err error) に変更され、releaseConn クロージャがエラーを引数として受け取るようになりました。
    • Stmt.connStmt 内で定義される releaseConn クロージャの定義が、エラー引数を受け取るように変更されました。
    • Stmt.Query メソッドのエラーパスで、s.db.putConn(ci, err) の代わりに releaseConn(err) が呼び出されるように変更されました。これが二重解放の主要な修正点です。
    • Rows 構造体の releaseConn フィールドの型が func() から func(error) に変更されました。
    • Rows.Close メソッド内で、rs.releaseConn()rs.releaseConn(err) に変更され、Rows.Close で発生したエラーが接続解放時に伝播されるようになりました。
  3. src/pkg/database/sql/sql_test.go:

    • init 関数が追加され、putConnHook を設定しています。このフックは、接続が二重に putConn に渡された場合にパニックを発生させることで、二重解放を検出します。
    • TestTxQueryInvalid という新しいテストケースが追加されました。これは、トランザクション内で不正なクエリを実行し、Stmt.Query がエラーを返した際に接続が適切に解放されることを検証します。
    • stack() ヘルパー関数が追加され、スタックトレースを取得するために使用されます。これは putConnHook で二重解放が検出された際に、どこから接続が解放されたかを特定するのに役立ちます。

コアとなるコードの解説

このコミットの核心は、database/sql パッケージにおける接続のライフサイクル管理、特にエラー発生時の接続の解放ロジックの改善にあります。

Stmt.Query の変更: 最も重要な変更は、src/pkg/database/sql/sql.goStmt.Query メソッド内です。 変更前:

	rowsi, err := si.Query(sargs)
	if err != nil {
		s.db.putConn(ci, err) // ここで直接 putConn が呼ばれていた
		return nil, err
	}

変更後:

	rowsi, err := si.Query(sargs)
	if err != nil {
		releaseConn(err) // ローカルの releaseConn クロージャを呼び出す
		return nil, err
	}

この変更により、Stmt.Query でエラーが発生した場合でも、接続の解放は Stmt.connStmt() で定義された releaseConn クロージャを通じて行われるようになりました。このクロージャは、Stmt が通常の DB 接続を使用しているか、それともトランザクション (Tx) 接続を使用しているかに応じて、適切な putConn または tx.releaseConn を呼び出します。これにより、トランザクション内の Stmt.Query エラー時に接続が二重に解放される問題が解決されました。

releaseConn シグネチャの変更とエラー伝播: Stmt.connStmt 関数内で定義される releaseConn クロージャのシグネチャが func() から func(error) に変更されました。 変更前:

func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, err error) {
    // ...
    releaseConn = func() { s.tx.releaseConn() } // または s.db.putConn(conn, nil)
    // ...
}

変更後:

func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(error), si driver.Stmt, err error) {
    // ...
    releaseConn = func(error) { s.tx.releaseConn() } // トランザクションの場合
    // ...
    releaseConn = func(err error) { s.db.putConn(conn, err) } // 通常のDB接続の場合
    // ...
}

この変更により、Stmt.QueryRows.Close などで発生したエラー(特に driver.ErrBadConn)が releaseConn を介して putConn に伝播されるようになりました。

putConn での ErrBadConn 処理: src/pkg/database/sql/sql.goputConn 関数は、渡されたエラーが driver.ErrBadConn である場合に、その接続をフリーリストに追加しないように変更されました。

func (db *DB) putConn(c driver.Conn, err error) {
	if err == driver.ErrBadConn { // ErrBadConn の場合、接続を破棄
		db.numOpen--
		c.Close()
		return
	}
	// ... 既存の接続プールへの追加ロジック ...
}

このロジックにより、不良な接続がプールに再利用可能なものとして戻されることがなくなり、アプリケーションが常に健全な接続を使用できるようになります。

テスト用 putConnHook: src/pkg/database/sql/sql_test.goinit 関数で設定される putConnHook は、デバッグとテストの強力なツールです。

func init() {
	// ...
	putConnHook = func(db *DB, c driver.Conn) {
		for _, oc := range db.freeConn {
			if oc == c {
				// 二重解放を検出した場合、パニック
				println("double free of conn. conflicts are:\nA) " + freedFrom[dbConn{db, c}] + "\n\nand\nB) " + stack())
				panic("double free of conn.")
			}
		}
		freedFrom[dbConn{db, c}] = stack()
	}
}

このフックは、putConn が呼び出されるたびに、その接続が既にフリーリストに存在しないかを確認します。もし存在すれば、それは二重解放を意味するため、パニックを発生させてテストを失敗させます。これにより、接続管理のバグを早期に発見できるようになります。

これらの変更は、database/sql パッケージの内部的な接続管理の正確性と堅牢性を大幅に向上させています。

関連リンク

参考にした情報源リンク