[インデックス 14651] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおける、データベース接続のエラーハンドリングに関する修正と、その修正を検証するためのテスト追加を目的としています。具体的には、database/sql
が driver.ErrBadConn
を Begin
メソッドが返した場合に、自動的にリトライを行うべきであることをテストで確認しています。
コミット
commit 19e2f26b21a8bb5f9cfed1808d25534d8fe88c47
Author: James David Chalfant <james.chalfant@gmail.com>
Date: Fri Dec 14 09:00:33 2012 -0800
database/sql: adds test for fix in issue 4433.
Tests that here should be automatic retries if a database
driver's connection returns ErrBadConn on Begin. See
"TestTxErrBadConn" in sql_test.go
R=golang-dev
CC=golang-dev
https://golang.org/cl/6942050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/19e2f26b21a8bb5f9cfed1808d25534d8fe88c47
元コミット内容
このコミットは、database/sql
パッケージにテストを追加するものです。このテストは、データベースドライバの Begin
メソッドが driver.ErrBadConn
を返した場合に、database/sql
パッケージが自動的にトランザクションの開始をリトライするべきであるという振る舞いを検証します。これは、issue 4433
で報告された問題に対する修正をテストするためのものです。
変更の背景
Go言語の database/sql
パッケージは、データベース操作のための汎用的なインターフェースを提供します。このパッケージは、様々なデータベースドライバ(MySQL, PostgreSQL, SQLiteなど)と連携して動作します。データベースとの接続は、ネットワークの不安定性、データベースサーバーの再起動、接続プールの問題など、様々な理由で一時的に無効になることがあります。
driver.ErrBadConn
は、Goの database/sql/driver
パッケージで定義されている特別なエラーです。このエラーは、ドライバがデータベースへの接続が不良であると判断した場合に返されます。例えば、ネットワーク接続が切断された、データベースサーバーがダウンした、または接続がタイムアウトしたなどの状況で発生します。
database/sql
パッケージの設計思想として、一時的な接続不良は透過的に処理され、アプリケーション開発者がこれらの低レベルなエラーを直接扱う必要がないようにすることが望ましいとされています。特に、トランザクションを開始する Begin
メソッドが ErrBadConn
を返した場合、database/sql
は新しい接続を確立してトランザクションの開始をリトライするべきです。これにより、アプリケーションは一時的な接続問題から回復し、より堅牢な動作が期待できます。
このコミットは、この自動リトライのメカニズムが正しく機能していることを確認するためのテストが不足していたため、そのテストを追加することを目的としています。コミットメッセージにある「issue 4433」は、この自動リトライが期待通りに動作しない、またはその振る舞いが明確にテストされていなかったという問題を示唆しています。
前提知識の解説
Go言語の database/sql
パッケージ
database/sql
パッケージは、Go言語でリレーショナルデータベースを操作するための標準インターフェースを提供します。このパッケージは、特定のデータベースシステムに依存しない抽象化レイヤーであり、実際のデータベースとの通信は、各データベースに対応する「ドライバ」によって行われます。
主要な概念:
sql.DB
: データベースへの抽象的な接続プールを表します。複数のゴルーチンから安全に利用できます。sql.Conn
: データベースへの単一の物理的な接続を表します。通常、sql.DB
から取得されます。sql.Tx
: データベーストランザクションを表します。sql.DB.Begin()
またはsql.Conn.Begin()
から取得されます。driver
インターフェース:database/sql
パッケージがデータベースドライバと通信するためのインターフェース群です。ドライバはこれらのインターフェースを実装することで、database/sql
パッケージと連携します。
driver.ErrBadConn
database/sql/driver
パッケージで定義されている特別なエラー変数です。ドライバがこのエラーを返した場合、database/sql
パッケージは、その接続が再利用不可能であると判断し、接続プールからその接続を破棄します。そして、必要に応じて新しい接続を確立しようとします。
このエラーは、以下のような状況でドライバによって返されることが期待されます。
- データベースサーバーとのネットワーク接続が切断された。
- データベースサーバーが再起動した。
- 接続がタイムアウトした。
- 認証情報が無効になった。
database/sql
パッケージは、driver.ErrBadConn
が返された場合に、特定の操作(例: Query
, Exec
, Begin
)を自動的にリトライするメカニズムを持っています。これにより、一時的な接続不良がアプリケーションに影響を与えるのを最小限に抑えます。
トランザクション (sql.Tx
)
トランザクションは、一連のデータベース操作を単一の論理的な作業単位としてまとめるメカニズムです。トランザクション内のすべての操作は成功するか、すべて失敗するかのいずれかです(ACID特性)。
sql.DB.Begin()
メソッドは、新しいトランザクションを開始します。このメソッドが呼び出された際に、database/sql
は接続プールから利用可能な接続を取得し、その接続上でトランザクションを開始します。もしこの接続が不良であった場合、ドライバの Begin
メソッドは driver.ErrBadConn
を返す可能性があります。この場合、database/sql
は別の接続を試みてリトライするべきです。
技術的詳細
このコミットは、database/sql
パッケージの自動リトライメカニズムをテストするために、fakedb_test.go
に偽のデータベースドライバ (fakeDriver
, fakeDB
, fakeConn
) を拡張し、sql_test.go
に新しいテストケース TestTxErrBadConn
を追加しています。
fakedb_test.go
の変更点
-
fakeDB
構造体へのbadConn
フィールドの追加:type fakeDB struct { name string mu sync.Mutex free []*fakeConn tables map[string]*table badConn bool // 新しく追加されたフィールド }
この
badConn
フィールドは、fakeDB
が現在「不良な接続」の状態をシミュレートしているかどうかを追跡するために使用されます。 -
fakeConn
構造体へのbad
フィールドの追加:type fakeConn struct { db *fakeDB closed bool stmtsMade int stmtsClosed int numPrepare int bad bool // 新しく追加されたフィールド }
この
bad
フィールドは、個々のfakeConn
インスタンスがbadConn
オプション付きで開かれたかどうかを示します。 -
fakeDriver.Open
メソッドの拡張: DSN (Data Source Name) に;badConn
オプションが指定された場合、新しく作成されるfakeConn
インスタンスのbad
フィールドがtrue
に設定されるようになりました。// Supports dsn forms: // <dbname> // <dbname>;<opts> (only currently supported option is `badConn`, // which causes driver.ErrBadConn to be returned on // every other conn.Begin()) func (d *fakeDriver) Open(dsn string) (driver.Conn, error) { // ... (既存のDSNパースロジック) ... conn := &fakeConn{db: db} if len(parts) >= 2 && parts[1] == "badConn" { conn.bad = true } return conn, nil }
これにより、テストケースで意図的に
ErrBadConn
を発生させるための接続を生成できるようになります。 -
fakeConn.isBad()
メソッドの追加: このヘルパーメソッドは、fakeConn
がbadConn
オプション付きで開かれている場合に、fakeDB
のbadConn
フィールドを交互に切り替えることで、ErrBadConn
を返すタイミングを制御します。func (c *fakeConn) isBad() bool { // if not simulating bad conn, do nothing if !c.bad { return false } // alternate between bad conn and not bad conn c.db.badConn = !c.db.badConn return c.db.badConn }
これにより、
Begin()
が呼び出されるたびに、ErrBadConn
を返すか、正常に動作するかが交互に切り替わります。これは、database/sql
がリトライを試みる際に、一度ErrBadConn
を受け取った後に、次の試行で正常な接続を得られる状況をシミュレートするために重要です。 -
fakeConn.Begin()
メソッドの変更:isBad()
メソッドの結果に基づいて、driver.ErrBadConn
を返すロジックが追加されました。func (c *fakeConn) Begin() (driver.Tx, error) { if c.isBad() { return nil, driver.ErrBadConn } // ... (既存のBeginロジック) ... }
これにより、
badConn
オプション付きで開かれた接続のBegin
メソッドが、交互にErrBadConn
を返すようになります。
sql_test.go
の変更点
-
TestTxErrBadConn
テスト関数の追加: この新しいテスト関数は、database/sql
がBegin
メソッドでErrBadConn
を受け取った場合に、自動的にリトライが行われることを検証します。// Tests fix for issue 4433, that retries in Begin happen when // conn.Begin() returns ErrBadConn func TestTxErrBadConn(t *testing.T) { // 1. `badConn` オプション付きでデータベースを開く db, err := Open("test", fakeDBName+";badConn") if err != nil { t.Fatalf("Open: %v", err) } defer closeDB(t, db) // 2. テーブル作成などの初期設定 exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool") stmt, err := db.Prepare("INSERT|t1|name=?,age=?") if err != nil { t.Fatalf("Stmt, err = %v, %v", stmt, err) } defer stmt.Close() // 3. トランザクションの開始 // ここで `db.Begin()` が呼び出される。 // 内部的には `fakeConn.Begin()` が呼び出され、 // 最初の呼び出しでは `ErrBadConn` が返されるが、 // `database/sql` が自動的にリトライし、 // 2回目の呼び出しで正常な接続を得ることを期待する。 tx, err := db.Begin() if err != nil { t.Fatalf("Begin = %v", err) } defer tx.Close() // トランザクションが正常に開始されたらクローズを予約 // 4. トランザクション内での操作 txs := tx.Stmt(stmt) defer txs.Close() _, err = txs.Exec("Bobby", 7) if err != nil { t.Fatalf("Exec = %v", err) } // 5. トランザクションのコミット err = tx.Commit() if err != nil { t.Fatalf("Commit = %v", err) } }
このテストの重要な点は、
db.Begin()
の呼び出しです。fakeConn.Begin()
はbadConn
オプションが有効な場合、最初の呼び出しでdriver.ErrBadConn
を返します。しかし、database/sql
パッケージがこのエラーを検知し、自動的にリトライを行うことで、テストは成功するはずです。もしリトライが行われなければ、db.Begin()
はエラーを返し、テストは失敗します。
コアとなるコードの変更箇所
このコミットは、既存のコードの振る舞いを変更するものではなく、その振る舞いを検証するためのテストコードを追加・修正しています。
-
src/pkg/database/sql/fakedb_test.go
:fakeDB
構造体にbadConn bool
フィールドを追加。fakeConn
構造体にbad bool
フィールドを追加。fakeDriver.Open
メソッドに、DSNの;badConn
オプションをパースし、fakeConn.bad
を設定するロジックを追加。fakeConn.isBad()
ヘルパーメソッドを追加。fakeConn.Begin()
メソッドに、isBad()
の結果に基づいてdriver.ErrBadConn
を返すロジックを追加。
-
src/pkg/database/sql/sql_test.go
:TestTxErrBadConn
テスト関数を追加。このテストは、badConn
オプション付きの偽のデータベース接続を使用し、db.Begin()
がdriver.ErrBadConn
を返しても、database/sql
が自動的にリトライしてトランザクションを正常に開始できることを検証します。
コアとなるコードの解説
このコミットの核心は、database/sql
パッケージが driver.ErrBadConn
を適切に処理し、透過的なリトライメカニズムを提供していることを証明するテストの追加です。
fakedb_test.go
での変更は、このテストを可能にするための「不良な接続」をシミュレートする機能を提供します。
fakeDB.badConn
とfakeConn.bad
は、シミュレーションの状態を管理します。fakeDriver.Open
での;badConn
オプションのパースは、テストが特定のシナリオをトリガーできるようにします。fakeConn.isBad()
は、Begin()
が呼び出されるたびにErrBadConn
を返すかどうかを交互に切り替えることで、database/sql
がリトライを試みる状況を正確に模倣します。これにより、一度失敗した後に成功する可能性のある接続をシミュレートできます。fakeConn.Begin()
でのdriver.ErrBadConn
の返却は、database/sql
が処理すべき具体的なエラー条件を提供します。
sql_test.go
での TestTxErrBadConn
は、このシミュレーションされたエラー条件の下で database/sql
の Begin
メソッドが期待通りに動作することを確認します。テストが成功するということは、db.Begin()
が内部的に fakeConn.Begin()
から ErrBadConn
を受け取ったにもかかわらず、database/sql
がそれを検知し、新しい接続を試みてトランザクションを正常に開始できたことを意味します。これは、アプリケーションが一時的なデータベース接続の問題に対してより堅牢になることを保証します。
このテストの追加により、将来的に database/sql
パッケージの内部実装が変更された場合でも、この重要なリトライ動作が維持されることが保証されます。
関連リンク
- Go言語
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go言語
database/sql/driver
パッケージのドキュメント: https://pkg.go.dev/database/sql/driver
参考にした情報源リンク
- Go言語の公式ドキュメント
database/sql
およびdatabase/sql/driver
パッケージのソースコード- Go言語のテストに関する一般的な知識
- Gitのコミット履歴と差分表示
driver.ErrBadConn
の挙動に関する一般的なデータベースドライバの知識