[インデックス 12529] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql パッケージとそのドライバインターフェースに ErrBadConn という新しいエラー型を導入し、データベース接続の堅牢性を向上させるための変更です。特に、データベース接続が不良状態になった際に、database/sql パッケージが自動的に新しい接続で操作を再試行するメカニズムを実装しています。
コミット
commit 9fb68a9a0a4229bc15688b448d0a5e8abff4b2dd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 8 10:09:52 2012 -0800
database/sql{,driver}: add ErrBadConn
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5785043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9fb68a9a0a4229bc15688b448d0a5e8abff4b2dd
元コミット内容
database/sql{,driver}: add ErrBadConn
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5785043
変更の背景
Goの database/sql パッケージは、データベースとのやり取りを抽象化し、ドライバを介して様々なデータベースに接続できるように設計されています。しかし、データベース接続はネットワークの問題、サーバーの再起動、アイドルタイムアウトなど、様々な理由で予期せず切断されたり、不良状態になったりすることがあります。
このコミット以前は、database/sql パッケージは、ドライバから返される特定のエラーが「接続が不良である」ことを意味するのか、それとも「操作が失敗した」ことを意味するのかを区別する明確なメカニズムを持っていませんでした。このため、不良な接続が接続プールに残り続け、後続の操作で同じエラーが発生し続ける可能性がありました。これはアプリケーションの堅牢性を低下させ、開発者が手動で接続の再試行ロジックを実装する必要があるという課題がありました。
ErrBadConn の導入は、この問題を解決するために考案されました。ドライバが ErrBadConn を返すことで、database/sql パッケージは、その接続が再利用不可能であることを認識し、接続プールから削除し、新しい接続で操作を自動的に再試行できるようになります。これにより、アプリケーションレベルでのエラーハンドリングが簡素化され、データベース操作の信頼性が向上します。
前提知識の解説
Goの database/sql パッケージ
database/sql パッケージは、Go言語でリレーショナルデータベースを操作するための標準インターフェースを提供します。このパッケージ自体は特定のデータベースの実装を含まず、データベースドライバを介してPostgreSQL, MySQL, SQLiteなど様々なデータベースと連携します。
主要な概念:
DB: データベースへのオープンな接続プールを表します。このオブジェクトは並行利用に対して安全であり、アプリケーションのライフサイクル全体で一度だけ作成されるべきです。driver.Driver: データベースドライバが実装すべきインターフェースです。Openメソッドを持ち、データベースへの新しい接続 (driver.Conn) を返します。driver.Conn: データベースへの単一の接続を表すインターフェースです。Prepare,Close,Beginなどのメソッドを持ちます。driver.Stmt: プリペアドステートメントを表すインターフェースです。- 接続プール:
database/sqlパッケージは、効率的なデータベースアクセスを可能にするために、内部的に接続プールを管理します。これにより、新しい接続を確立するオーバーヘッドを削減し、既存の接続を再利用できます。
データベース接続のライフサイクルとエラーハンドリング
データベース接続は、確立、使用、解放のライフサイクルを持ちます。この過程で様々なエラーが発生する可能性があります。
- ネットワークエラー: データベースサーバーへの到達不能、接続タイムアウトなど。
- 認証エラー: 不正なユーザー名やパスワード。
- クエリ実行エラー: SQL構文エラー、制約違反など。
- 接続不良: データベースサーバーが接続を閉じた、サーバーが再起動した、アイドルタイムアウトにより接続が切断されたなど。
特に「接続不良」の場合、アプリケーションはすぐにその接続を再利用しようとすると、再度エラーに遭遇します。このような状況では、その接続を破棄し、新しい接続を確立して操作を再試行することが望ましいです。
冪等性 (Idempotency)
操作の再試行を実装する上で重要な概念が「冪等性」です。冪等な操作とは、複数回実行しても結果が一度実行した場合と同じになる操作のことです。
例えば、SELECT クエリは通常冪等です。何度実行してもデータベースの状態は変わりません。しかし、INSERT や UPDATE、DELETE といった変更操作は、通常は冪等ではありません。もし INSERT 操作が一度成功したにも関わらず、ネットワークエラーでクライアントに結果が届かなかった場合、クライアントが再試行すると同じデータが二重に挿入される可能性があります。
ErrBadConn のドキュメントにもあるように、「データベースサーバーが操作を実行した可能性がある場合は、ErrBadConn を返すべきではない」という注意書きは、この冪等性の問題を回避するためのものです。つまり、ドライバは、操作が実際にデータベース側で実行されたかどうかが不明な場合(例えば、クエリ送信後にネットワークが切断された場合など)には ErrBadConn を返すべきではありません。ErrBadConn は、操作がデータベースに到達する前に接続が明らかに不良であった場合にのみ使用されるべきです。
技術的詳細
このコミットの技術的詳細は、主に database/sql/driver/driver.go と database/sql/sql.go の2つのファイルにわたる変更に集約されます。
database/sql/driver/driver.go の変更
-
ErrBadConnの定義:var ErrBadConn = errors.New("driver: bad connection")driverパッケージにErrBadConnという新しい公開変数(エラー)が追加されました。これは、ドライバがdatabase/sqlパッケージに対して、現在使用しているdriver.Connインスタンスが不良状態であり、再利用すべきではないことを通知するためのシグナルとして機能します。 -
ErrBadConnの使用に関するコメント: 追加されたコメントは、ドライバ開発者向けにErrBadConnをいつ、どのように使用すべきかを明確に指示しています。- 「
ErrBadConnは、driver.Connが不良状態(例:サーバーが以前に接続を閉じた)であることをsqlパッケージに通知するためにドライバによって返されるべきであり、sqlパッケージは新しい接続で再試行すべきである。」 - 「重複操作を防ぐため、データベースサーバーが操作を実行した可能性がある場合は、
ErrBadConnを返すべきではない。」 - 「サーバーがエラーを返した場合でも、
ErrBadConnを返すべきではない。」 これらの指示は、冪等性の原則を尊重し、不必要な再試行やデータ重複を防ぐためのものです。ErrBadConnは、接続自体が使用不可能であるとドライバが判断した場合にのみ使用されるべきであり、特定のクエリの失敗を示すものではありません。
- 「
database/sql/sql.go の変更
sql.go では、ErrBadConn を利用して接続プール管理と操作の再試行ロジックが強化されています。
-
putConn関数の変更:// putConn adds a connection to the db's free pool. // err is optionally the last error that occured on this connection. func (db *DB) putConn(c driver.Conn, err error) { if err == driver.ErrBadConn { // Don't reuse bad connections. return } // ... 既存の接続プールへの追加ロジック ... }putConn関数は、接続を接続プールに戻す役割を担っています。この変更により、putConnはerrパラメータを受け取るようになりました。もしerrがdriver.ErrBadConnと等しい場合、その接続は接続プールに戻されずに破棄されます。これにより、不良な接続がプールに残り、後続の操作で再利用されることを防ぎます。 -
Prepare,Exec,Beginメソッドの再試行ロジック: これらのメソッドは、データベース操作の入り口となる重要な関数です。変更後、これらの関数は内部的にヘルパー関数 (db.prepare,db.exec,db.begin) を呼び出し、その結果がdriver.ErrBadConnであった場合に最大10回まで操作を再試行するようになりました。// 例: Prepare メソッド func (db *DB) Prepare(query string) (*Stmt, error) { var stmt *Stmt var err error for i := 0; i < 10; i++ { // 最大10回の再試行 stmt, err = db.prepare(query) if err != driver.ErrBadConn { // ErrBadConn でなければループを抜ける break } } return stmt, err }この再試行ループは、
ErrBadConnが返された場合にのみトリガーされます。これにより、一時的な接続の問題によって操作が失敗した場合でも、アプリケーションが自動的に回復し、新しい健全な接続で操作を完了できる可能性が高まります。再試行回数が10回に制限されているのは、無限ループを防ぎ、永続的な問題の場合には最終的にエラーを返すためです。 -
Stmt.connStmtの再試行ロジック:Stmt(プリペアドステートメント) が新しい接続を必要とする場合 (Prepare操作など) にも、同様の再試行ロジックが導入されました。// Stmt.connStmt 内の変更 // ... if err == driver.ErrBadConn && i < 10 { continue // ErrBadConn なら再試行 } // ...これにより、プリペアドステートメントの準備中に接続が不良であった場合も、自動的に再試行が行われます。
-
putConnへのerrパラメータの伝播:Prepare,Exec,Begin,Query,Close(Stmt),close(Tx) など、接続をプールに戻す可能性のあるすべての場所で、putConn関数がエラーパラメータを受け取るように変更されました。これにより、操作中に発生したエラー(特にErrBadConn)がputConnに適切に伝達され、不良な接続がプールから確実に削除されるようになります。
これらの変更により、database/sql パッケージは、ドライバが報告する接続不良に対してよりインテリジェントに対応できるようになり、アプリケーション開発者は接続の堅牢性に関する複雑なロジックを自身で実装する必要がなくなりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。
-
src/pkg/database/sql/driver/driver.go:ErrBadConn変数の追加とそのドキュメンテーション。
-
src/pkg/database/sql/sql.go:putConn関数のシグネチャ変更と、ErrBadConnに基づく接続破棄ロジックの追加。Prepare,Exec,Beginメソッドに、ErrBadConnが返された場合の再試行ループ(最大10回)の追加。Stmt.connStmt内でのPrepare操作に対するErrBadConnの再試行ロジックの追加。putConnを呼び出すすべての箇所で、適切なエラーが渡されるように変更。
コアとなるコードの解説
src/pkg/database/sql/driver/driver.go
// ErrBadConn should be returned by a driver to signal to the sql
// package that a driver.Conn is in a bad state (such as the server
// having earlier closed the connection) and the sql package should
// retry on a new connection.
//
// To prevent duplicate operations, ErrBadConn should NOT be returned
// if there's a possibility that the database server might have
// performed the operation. Even if the server sends back an error,
// you shouldn't return ErrBadConn.
var ErrBadConn = errors.New("driver: bad connection")
このコードは、database/sql/driver パッケージに新しいエラー ErrBadConn を定義しています。このエラーは、ドライバが database/sql パッケージに対して、現在のデータベース接続が使用不可能であることを明示的に通知するためのものです。重要なのは、このエラーが返されるべき状況に関する詳細なコメントです。これは、操作の冪等性を考慮し、データベース側で既に操作が実行された可能性がある場合にはこのエラーを返すべきではないことを強調しています。これにより、database/sql パッケージが自動的に再試行を行った際に、意図しない重複操作が発生するのを防ぎます。
src/pkg/database/sql/sql.go
putConn 関数の変更
// putConn adds a connection to the db's free pool.
// err is optionally the last error that occured on this connection.
func (db *DB) putConn(c driver.Conn, err error) {
if err == driver.ErrBadConn {
// Don't reuse bad connections.
return // 接続プールに戻さずに破棄
}
db.mu.Lock()
if n := len(db.freeConn); !db.closed && n < db.maxIdleConns() {
db.freeConn = append(db.freeConn, c) // 接続プールに追加
db.mu.Unlock()
return
}
// ... (ロック解除と接続クローズのロジック) ...
db.mu.Unlock() // ロック解除の移動
c.Close() // 接続をクローズ
}
putConn 関数は、データベース接続をアイドル接続プールに戻す役割を担います。この変更の最も重要な点は、err パラメータが追加されたことです。もし渡された err が driver.ErrBadConn であった場合、その接続は接続プールに戻されず、実質的に破棄されます。これにより、不良な接続がプールに残り続け、後続の操作で再利用されることを防ぎます。また、db.mu.Unlock() の位置が変更され、db.closeConn(c) の呼び出し前にロックが解放されるようになりました。これは、c.Close() がブロックする可能性があるため、ロックを保持し続けることによるデッドロックやパフォーマンスの問題を避けるためと考えられます。
Prepare, Exec, Begin メソッドの再試行ロジック
// 例: Prepare メソッドの変更
func (db *DB) Prepare(query string) (*Stmt, error) {
var stmt *Stmt
var err error
for i := 0; i < 10; i++ { // 最大10回の再試行ループ
stmt, err = db.prepare(query) // 実際の準備処理は db.prepare で行われる
if err != driver.ErrBadConn {
break // ErrBadConn 以外ならループを抜ける
}
}
return stmt, err
}
// db.prepare, db.exec, db.begin は実際の処理を行うヘルパー関数
func (db *DB) prepare(query string) (stmt *Stmt, err error) {
ci, err := db.conn() // 接続を取得
if err != nil {
return nil, err
}
defer db.putConn(ci, err) // 接続をプールに戻す際にエラーを渡す
si, err := ci.Prepare(query) // ドライバの Prepare を呼び出す
// ...
}
Prepare, Exec, Begin といった主要なデータベース操作メソッドは、内部的に db.prepare, db.exec, db.begin といったヘルパー関数を呼び出すようになりました。これらのヘルパー関数からの戻り値が driver.ErrBadConn であった場合、外側のループが最大10回まで操作を再試行します。これは、一時的な接続不良によって操作が失敗した場合に、database/sql パッケージが自動的に新しい接続を取得し、操作を再試行することで、アプリケーションの回復力を高めるための重要なメカニズムです。再試行回数が10回に制限されているのは、永続的な接続問題の場合に無限ループに陥るのを防ぐためです。また、defer db.putConn(ci, err) のように、putConn にエラーを渡すことで、操作中に接続が不良になった場合にその接続がプールから適切に削除されるようにしています。
Stmt.connStmt の変更
// Stmt.connStmt 内の変更
// ...
if !match { // 既存の接続が利用できない場合
for i := 0; ; i++ { // 無限ループに見えるが、ErrBadConn でないか10回で break
ci, err := s.db.conn() // 新しい接続を取得
if err != nil {
return nil, nil, nil, err
}
si, err := ci.Prepare(s.query) // 新しい接続で Prepare を試みる
if err == driver.ErrBadConn && i < 10 { // ErrBadConn かつ再試行回数が10回未満なら
continue // 再試行
}
if err != nil { // ErrBadConn 以外のエラーなら
return nil, nil, nil, err // エラーを返す
}
// ... (成功した場合の処理) ...
break // 成功したらループを抜ける
}
}
// ...
Stmt.connStmt は、プリペアドステートメントが実行される際に、適切な接続とステートメントを取得する役割を担います。この部分にも ErrBadConn の再試行ロジックが追加されました。もし ci.Prepare(s.query) の呼び出しが ErrBadConn を返した場合、最大10回まで新しい接続で Prepare 操作を再試行します。これにより、プリペアドステートメントの準備段階で接続が不良であった場合でも、自動的に回復を試みることができます。
これらの変更は、Goの database/sql パッケージが、データベース接続の信頼性と堅牢性を大幅に向上させるための重要なステップでした。
関連リンク
- Go
database/sqlパッケージのドキュメント: https://pkg.go.dev/database/sql - Go
database/sql/driverパッケージのドキュメント: https://pkg.go.dev/database/sql/driver - 元の Gerrit Change-ID: https://golang.org/cl/5785043
参考にした情報源リンク
- Go
database/sqlパッケージのソースコード (特にsql.goとdriver/driver.go) - Go言語の公式ドキュメント
- 一般的なデータベース接続プールとエラーハンドリングに関する情報
- 冪等性に関する一般的なプログラミング概念