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

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

このコミットは、Go言語の database/sql パッケージにおける重要なバグ修正です。具体的には、Rows オブジェクトがクローズされる際に、関連する driver.Stmt (ステートメント) がクローズされる前に、そのステートメントが使用していた driver.Conn (データベース接続) がコネクションプールに解放されてしまう競合状態を解消します。これにより、一部のデータベースドライバー(特に lib/pq のような、Stmt.Close 時にネットワーク通信を行うもの)で発生していた、解放済みコネクション上での不正な操作によるパニックやエラーを防ぎます。

コミット

commit 36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 15 14:06:41 2013 -0700

    database/sql: close driver Stmt before releasing Conn
    
    From the issue, which describes it as well as I could:
    
    database/sql assumes that driver.Stmt.Close does not need the
    connection.
    
    see database/sql/sql.go:1308:
    
    This puts the Rows' connection back into the idle pool, and
    then calls the driver.Stmt.Close method of the Stmt it belongs
    to.  In the postgresql driver implementation
    (https://github.com/lib/pq), Stmt.Close communicates with the
    server (on the connection that was just put back into the idle
    pool).  Most of the time, this causes no problems, but if
    another goroutine makes a query at the right (wrong?) time,
    chaos results.
    
    In any case, traffic is being sent on "free" connections
    shortly after they are freed, leading to race conditions that
    kill the driver code.
    
    Fixes #5283
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8633044

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

https://github.com/golang/go/commit/36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114

元コミット内容

commit 36d3bef8a3b2a3b7b2662e5b2fd7abbf70c47114
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 15 14:06:41 2013 -0700

    database/sql: close driver Stmt before releasing Conn
    
    From the issue, which describes it as well as I could:
    
    database/sql assumes that driver.Stmt.Close does not need the
    connection.
    
    see database/sql/sql.go:1308:
    
    This puts the Rows' connection back into the idle pool, and
    then calls the driver.Stmt.Close method of the Stmt it belongs
    to.  In the postgresql driver implementation
    (https://github.com/lib/pq), Stmt.Close communicates with the
    server (on the connection that was just put back into the idle
    pool).  Most of the time, this causes no problems, but if
    another goroutine makes a query at the right (wrong?) time,
    chaos results.
    
    In any case, traffic is being sent on "free" connections
    shortly after they are freed, leading to race conditions that
    kill the driver code.
    
    Fixes #5283
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8633044

変更の背景

Go言語の database/sql パッケージは、データベース操作のための汎用的なインターフェースを提供します。このパッケージは、具体的なデータベースドライバー(例: PostgreSQL用の lib/pq、MySQL用の go-sql-driver/mysql など)と連携して動作します。

問題は、Rows オブジェクト(クエリ結果の行をイテレートするために使用される)がクローズされる際の内部処理にありました。以前の実装では、Rows.Close() メソッド内で、まず Rows が使用していたデータベース接続 (driver.Conn) をコネクションプールに解放し、その後で関連する driver.Stmt (プリペアドステートメントなど) をクローズしようとしていました。

しかし、database/sql パッケージの設計上の仮定として、「driver.Stmt.Close() メソッドは、そのステートメントが元々関連付けられていたデータベース接続を必要としない」というものがありました。この仮定は、多くのデータベースドライバーでは問題になりませんでしたが、lib/pq のような一部のPostgreSQLドライバーの実装では、Stmt.Close() が実際にデータベースサーバーとの通信を伴う場合がありました。例えば、プリペアドステートメントをサーバー側で明示的に解放するプロトコル操作を行う場合などです。

この状況下で、driver.Conn がコネクションプールに解放された直後に driver.Stmt.Close() が呼び出されると、以下の競合状態が発生する可能性がありました。

  1. Rows.Close()driver.Conn をアイドルプールに返却する。
  2. ほぼ同時に、別のゴルーチンがコネクションプールからその driver.Conn を取得し、新しいクエリを実行し始める。
  3. その間に、元の Rows.Close() の処理が続き、解放済みであるはずの driver.Conn を使用して driver.Stmt.Close() が実行される。

結果として、driver.Stmt.Close() が、すでに別のゴルーチンによって使用されている、あるいは不正な状態にあるコネクションに対して操作を行おうとし、「解放済みコネクション上でのトラフィック送信」や「ドライバーコードのクラッシュ」といった予期せぬ動作やパニックを引き起こしていました。これは、データベース操作の信頼性と安定性を著しく損なう深刻なバグでした。

この問題は Go issue #5283 として報告され、このコミットによって修正されました。

前提知識の解説

このコミットの理解には、以下のGo言語の database/sql パッケージに関する知識が役立ちます。

  • database/sql パッケージ: Go標準ライブラリの一部で、SQLデータベースとの対話のための抽象レイヤーを提供します。これにより、アプリケーションコードは特定のデータベースドライバーに依存することなく、汎用的なインターフェースを通じてデータベースを操作できます。
  • driver.Conn: データベースへの単一の物理的な接続を表すインターフェースです。database/sql パッケージは、これらの接続をプールして再利用することで、接続確立のオーバーヘッドを削減します。
  • driver.Stmt: プリペアドステートメント(Prepared Statement)を表すインターフェースです。SQLクエリを事前にコンパイルし、パラメータをバインドして複数回実行するために使用されます。これにより、パフォーマンスが向上し、SQLインジェクション攻撃を防ぐことができます。
  • Rows: Query メソッドの実行結果を表すインターフェースです。結果セットの行をイテレートするために使用され、すべての行が処理された後、またはエラーが発生した場合には Close() メソッドを呼び出してリソースを解放する必要があります。
  • コネクションプーリング (Connection Pooling): データベース接続の確立はコストの高い操作であるため、database/sql パッケージは内部的にデータベース接続のプールを管理します。アプリケーションが接続を要求すると、プールから既存のアイドル接続が提供されます。接続が不要になると、プールに返却され、他のリクエストのために再利用されます。
  • 競合状態 (Race Condition): 複数のゴルーチン(Goの軽量スレッド)が共有リソース(この場合はデータベース接続)に同時にアクセスし、そのアクセス順序によってプログラムの最終結果が非決定的に変わってしまう状態を指します。競合状態はデバッグが困難なバグの一般的な原因です。
  • lib/pq: Go言語でPostgreSQLデータベースに接続するための人気のあるドライバーです。このドライバーは、PostgreSQLのプロトコルに準拠しており、プリペアドステートメントのクローズなど、一部の操作でサーバーとの通信を必要とすることがあります。

技術的詳細

このバグの核心は、database/sql パッケージの Rows.Close() メソッドにおけるリソース解放の順序にありました。

  1. 旧実装の順序: Rows.Close() が呼び出されると、まず rs.rowsi.Close() が実行され、次に rs.releaseConn(err) が呼び出されて、Rows が使用していた driver.Conn がコネクションプールに返却されていました。その後、もし rs.closeStmt (関連する driver.Stmt) が存在すれば、rs.closeStmt.Close() が呼び出されていました。

    // 旧実装の擬似コード
    func (rs *Rows) Close() error {
        // ...
        err := rs.rowsi.Close()
        rs.releaseConn(err) // (1) コネクションをプールに解放
        if rs.closeStmt != nil {
            rs.closeStmt.Close() // (2) ステートメントをクローズ
        }
        // ...
    }
    
  2. 問題の発生: lib/pq のような一部のドライバーでは、driver.Stmt.Close() が実際にデータベースサーバーに対してネットワーク通信を行う必要がありました。この通信は、当然ながらそのステートメントが関連付けられている driver.Conn を通じて行われます。 しかし、旧実装では Stmt.Close() が呼び出される前に Conn がすでにプールに解放されていました。これにより、以下のシナリオが発生し得ました。

    • Conn がプールに解放された直後、別のゴルーチンがその Conn をプールから取得し、新しいクエリを実行し始める。
    • その間に、元の Rows.Close() の処理が続き、Stmt.Close() が実行される。このとき、Stmt.Close() は、すでに別のゴルーチンによって使用されている、またはプールに返却されたために内部状態がリセットされている Conn を使用しようとする。
    • 結果として、不正なコネクション状態での通信試行により、パニックやデータベースドライバーレベルでのエラーが発生しました。コミットメッセージにある「chaos results」や「traffic is being sent on "free" connections shortly after they are freed」という表現は、この競合状態による不安定な挙動を指しています。
  3. 修正と解決策: このコミットでは、Rows.Close() メソッド内の driver.Stmt.Close()rs.releaseConn() の呼び出し順序を入れ替えることで問題を解決しました。

    // 新実装の擬似コード
    func (rs *Rows) Close() error {
        // ...
        err := rs.rowsi.Close()
        if rs.closeStmt != nil {
            rs.closeStmt.Close() // (1) まずステートメントをクローズ
        }
        rs.releaseConn(err) // (2) その後コネクションをプールに解放
        // ...
    }
    

    この変更により、driver.Stmt.Close() が呼び出される時点では、関連する driver.Conn はまだ Rows オブジェクトによって排他的に保持されており、他のゴルーチンに再利用されることはありません。これにより、Stmt.Close() が安全に、かつ意図した通りにデータベースサーバーとの通信を完了できるようになります。

  4. テストの追加と強化: この修正を検証するために、テストコードも強化されました。

    • src/pkg/database/sql/fakedb_test.go には、fakeConn.Close() が失敗した場合に testing.T.Errorf を呼び出す setStrictFakeConnClose というヘルパー関数が追加されました。これにより、テスト中にコネクションのクローズが失敗した場合に、より厳密にエラーを検出できるようになりました。また、fakeStmt.Close() には、s.c == nils.c.db == nil の場合にパニックを起こすチェックが追加され、ステートメントがクローズされる時点でコネクションが有効であることを保証するようになりました。
    • src/pkg/database/sql/sql_test.go には TestRowsCloseOrder という新しいテストケースが追加されました。このテストは、db.SetMaxIdleConns(0) を設定してコネクションプールを無効にし、setStrictFakeConnClose(t) を呼び出すことで、コネクションが即座にクローズされ、Stmt.Close がコネクション解放前に実行されることを厳密に検証します。

これらの変更により、database/sql パッケージの堅牢性が向上し、特定のデータベースドライバーとの組み合わせで発生していた競合状態が解消されました。

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

src/pkg/database/sql/sql.go ファイルの Rows.Close() メソッドにおける変更がコアです。

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -1311,10 +1311,10 @@ func (rs *Rows) Close() error {
 	}
 	rs.closed = true
 	err := rs.rowsi.Close()
-	rs.releaseConn(err)
 	if rs.closeStmt != nil {
 		rs.closeStmt.Close()
 	}
+	rs.releaseConn(err)
 	return err
 }

コアとなるコードの解説

上記の差分が示すように、Rows.Close() メソッド内で、rs.releaseConn(err) の呼び出し位置が変更されました。

  • 変更前:

    err := rs.rowsi.Close()
    rs.releaseConn(err) // コネクションをプールに解放
    if rs.closeStmt != nil {
        rs.closeStmt.Close() // その後、ステートメントをクローズ
    }
    

    この順序では、rs.closeStmt.Close() が呼び出される時点で、rs が保持していた driver.Conn はすでにコネクションプールに返却されており、別のゴルーチンによって再利用される可能性がありました。もし rs.closeStmt.Close() がそのコネクションを必要とする場合(例: lib/pq のようにサーバーにメッセージを送る場合)、競合状態が発生し、不正なコネクションに対して操作が行われることになります。

  • 変更後:

    err := rs.rowsi.Close()
    if rs.closeStmt != nil {
        rs.closeStmt.Close() // まずステートメントをクローズ
    }
    rs.releaseConn(err) // その後、コネクションをプールに解放
    

    この新しい順序では、rs.closeStmt.Close() が呼び出される前に rs.releaseConn(err) が実行されることはありません。つまり、driver.Stmt.Close() が実行される時点では、関連する driver.Conn はまだ Rows オブジェクトによって排他的に保持されています。これにより、Stmt.Close() が安全に、かつ意図した通りに、そのコネクションを通じて必要なクリーンアップ操作(例えば、データベースサーバー上のプリペアドステートメントの解放)を完了できるようになります。すべてのステートメント関連のクリーンアップが完了した後にのみ、コネクションがプールに返却されるため、競合状態が解消されます。

このシンプルな順序の変更が、database/sql パッケージの安定性と信頼性を大幅に向上させました。

関連リンク

参考にした情報源リンク