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

[インデックス 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 クエリは通常冪等です。何度実行してもデータベースの状態は変わりません。しかし、INSERTUPDATEDELETE といった変更操作は、通常は冪等ではありません。もし INSERT 操作が一度成功したにも関わらず、ネットワークエラーでクライアントに結果が届かなかった場合、クライアントが再試行すると同じデータが二重に挿入される可能性があります。

ErrBadConn のドキュメントにもあるように、「データベースサーバーが操作を実行した可能性がある場合は、ErrBadConn を返すべきではない」という注意書きは、この冪等性の問題を回避するためのものです。つまり、ドライバは、操作が実際にデータベース側で実行されたかどうかが不明な場合(例えば、クエリ送信後にネットワークが切断された場合など)には ErrBadConn を返すべきではありません。ErrBadConn は、操作がデータベースに到達する前に接続が明らかに不良であった場合にのみ使用されるべきです。

技術的詳細

このコミットの技術的詳細は、主に database/sql/driver/driver.godatabase/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 関数は、接続を接続プールに戻す役割を担っています。この変更により、putConnerr パラメータを受け取るようになりました。もし errdriver.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つのファイルに集中しています。

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

    • ErrBadConn 変数の追加とそのドキュメンテーション。
  2. 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 パラメータが追加されたことです。もし渡された errdriver.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 パッケージのソースコード (特に sql.godriver/driver.go)
  • Go言語の公式ドキュメント
  • 一般的なデータベース接続プールとエラーハンドリングに関する情報
  • 冪等性に関する一般的なプログラミング概念