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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける、データベースステートメント (Stmt) と結果セット (Rows) のライフタイム管理に関する重要な修正を含んでいます。変更されたファイルは以下の通りです。

  • src/pkg/database/sql/driver/driver.go: driver.Stmt インターフェースの Close メソッドに関するコメントが更新され、その契約が簡素化されました。
  • src/pkg/database/sql/sql.go: DB 構造体に参照カウンティングと依存関係追跡のための新しいフィールド (outConn, dep, onConnPut, lastPut) が追加され、finalCloser インターフェースと depSet 型が導入されました。また、addDep, removeDep, noteUnusedDriverStatement, putConn, prepare, Stmt.Exec, Stmt.Query, Stmt.Close, Stmt.finalClose, stack といったメソッドが実装または修正され、リソースのライフタイム管理ロジックが大幅に強化されました。
  • src/pkg/database/sql/sql_test.go: closeDB 関数の修正と、以前はスキップされていた TestCloseStmtBeforeRows テストが有効化されました。これは、このコミットが修正対象とする問題が解決されたことを示しています。

コミット

database/sql: refcounting and lifetime fixes

Simplifies the contract for Driver.Stmt.Close in
the process of fixing issue 3865.

Fixes #3865
Update #4459 (maybe fixes it; uninvestigated)

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7363043

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

https://github.com/golang/go/commit/f7a7716317dede4687a7fed38aea8d256f4d09e5

元コミット内容

commit f7a7716317dede4687a7fed38aea8d256f4d09e5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Feb 20 15:35:27 2013 -0800

    database/sql: refcounting and lifetime fixes
    
    Simplifies the contract for Driver.Stmt.Close in
    the process of fixing issue 3865.
    
    Fixes #3865
    Update #4459 (maybe fixes it; uninvestigated)
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7363043

変更の背景

Goの database/sql パッケージは、SQLデータベースへのアクセスを抽象化するための標準インターフェースを提供します。このパッケージは、特定のデータベース実装に依存しない汎用的なAPIを提供し、実際のデータベース操作は driver パッケージで定義されたインターフェースを実装する外部ドライバに委ねられます。

このコミットが対処する主要な問題は、Go Issue 3865 (golang.org/issue/3865) で報告された Stmt (プリペアドステートメント) のライフタイム管理に関するものです。具体的には、Stmt.Close() メソッドが呼び出された際に、その Stmt から生成された Rows オブジェクトがまだ使用中であるにもかかわらず、基盤となるデータベースドライバのステートメントが誤ってクローズされてしまう可能性がありました。

この問題は、以下のようなシナリオで顕在化しました。

  1. DB.Prepare()Stmt を作成。
  2. Stmt.Query()Rows を取得。
  3. Stmt.Close() を呼び出す。
  4. Rows からデータを読み取ろうとするが、基盤のステートメントが既にクローズされているためエラーが発生する。

このような状況は、リソースリークや、使用中のリソースが予期せず解放されることによる競合状態、アプリケーションのクラッシュを引き起こす可能性がありました。database/sql パッケージは、複数のゴルーチンから安全に利用されることを前提としているため、このようなライフタイムの競合は深刻な問題でした。

このコミットの目的は、StmtRows の間の依存関係を適切に管理するための堅牢な参照カウンティングメカニズムを導入し、Stmt.Close() の契約をより安全で予測可能なものにすることです。これにより、ドライバ開発者は Stmt.Close の実装について複雑な考慮をする必要がなくなり、database/sql パッケージの全体的な信頼性が向上します。

前提知識の解説

Goの database/sql パッケージの基本構造

Goの database/sql パッケージは、データベース操作のための高レベルな抽象化を提供します。

  • DB: データベースへの接続プールを表します。sql.Open() で作成され、アプリケーション全体で共有されることが一般的です。内部で複数の driver.Conn を管理し、必要に応じて接続を再利用します。
  • driver.Conn: データベースへの単一の物理的な接続を表すインターフェースです。ドライバがこのインターフェースを実装します。
  • Stmt: プリペアドステートメントを表します。SQLクエリを事前にコンパイル(準備)し、パラメータをバインドして複数回実行するために使用されます。これにより、SQLインジェクション攻撃を防ぎ、クエリの実行効率を向上させます。DB.Prepare() または Tx.Prepare() で作成されます。
  • driver.Stmt: ドライバが実装するプリペアドステートメントのインターフェースです。Stmt オブジェクトは内部的に一つ以上の driver.Stmt を保持します。
  • Rows: クエリ結果の行セットを表します。Stmt.Query() または DB.Query() で返されます。結果セットをイテレートし、各行のデータをスキャンするために使用されます。Next() で次の行に進み、Scan() でデータを変数に読み込みます。使用後は必ず Close() を呼び出してリソースを解放する必要があります。
  • driver.Rows: ドライバが実装する結果セットのインターフェースです。Rows オブジェクトは内部的に driver.Rows を保持します。

参照カウンティング (Reference Counting)

参照カウンティングは、オブジェクトがどれだけの他のオブジェクトから参照されているかを追跡するメモリ管理の技術です。オブジェクトへの参照が追加されるたびにカウントが増加し、参照が削除されるたびにカウントが減少します。カウントがゼロになったとき、そのオブジェクトはもはや使用されていないと判断され、安全に解放(クリーンアップ)できます。

Go言語にはガベージコレクタがあり、通常はメモリ管理を自動で行いますが、データベース接続、ファイルハンドル、ネットワークソケットなどのOSリソースは、明示的なクローズ操作が必要です。これらのリソースはガベージコレクタの管理外にあるため、プログラマが適切に解放する必要があります。しかし、複数のオブジェクトが同じ基盤リソースに依存している場合、いつそのリソースを安全にクローズできるかを判断するのは複雑になります。このような場合に、参照カウンティングの概念を適用することで、依存関係がすべて解消されたときにのみリソースを解放する、という安全なメカニズムを構築できます。

このコミットでは、StmtRows を生成し、その Rows がアクティブである間は Stmt の基盤となるドライバステートメントをクローズしてはならない、という依存関係を管理するために、参照カウンティングに似たメカニズムが導入されています。

技術的詳細

このコミットの主要な目的は、database/sql パッケージにおける StmtRows のライフタイム管理の堅牢性を向上させることです。これを実現するために、以下の技術的詳細が導入・変更されました。

  1. DB 構造体の拡張とライフタイム管理の中央化: DB 構造体は、単なる接続プールとしてだけでなく、StmtRows といったリソースのライフタイムを管理する中心的な役割を担うようになりました。

    • outConn map[driver.Conn]bool: 現在アプリケーションによって使用中(プールから取り出されている)の driver.Conn を追跡します。これにより、接続が誤って二重にプールに戻されるのを防ぎ、接続の正確な状態を把握できます。
    • dep map[finalCloser]depSet: finalCloser インターフェースを実装するオブジェクト間の依存関係を追跡するためのマップです。xdep に依存している場合、xfinalClose メソッドは dep が解放されるまで呼び出されません。これは、Stmt が生成した Rows がアクティブである間は Stmt をクローズしない、というロジックの基盤となります。
    • onConnPut map[driver.Conn][]func(): 特定の driver.Conn が接続プールに戻される際に実行されるべきクリーンアップ関数(例: ドライバのステートメントのクローズ)をキューイングするために使用されます。これにより、接続が安全に再利用可能になった時点で、その接続に関連するリソースを解放できます。
    • lastPut map[driver.Conn]string: デバッグ目的で、接続が最後にプールに戻された際のスタックトレースを記録します。これは、putConn で二重に接続が戻されるなどの異常な状況を診断するのに役立ちます。
  2. finalCloser インターフェースと depSet 型の導入:

    • type finalCloser interface { finalClose() error }: このインターフェースは、オブジェクトの参照カウントがゼロになり、完全に解放される直前に行うべきクリーンアップ処理を抽象化します。Stmt 型がこのインターフェースを実装し、その finalClose メソッドで基盤となるドライバステートメントをクローズするロジックを含みます。
    • type depSet map[interface{}]bool: finalCloser オブジェクトが依存している他のオブジェクトのセットを表すために使用される型です。
  3. 依存関係管理メカニズム (addDep, removeDep):

    • (*DB).addDep(x finalCloser, dep interface{}): xdep に依存していることを db.dep マップに記録します。これにより、xfinalClosedep が解放される前に呼び出されるのを防ぎます。
    • (*DB).removeDep(x finalCloser, dep interface{}) error: xdep への依存を解消したことを db.dep マップから削除します。もし x のすべての依存関係が解消された場合(つまり、x の参照カウントがゼロになった場合)、x.finalClose() が呼び出され、その結果が返されます。このメカニズムにより、リソースの安全な解放が保証されます。
  4. StmtRows のライフタイム管理の変更:

    • Stmt.Query() での依存関係の確立: Stmt.Query() メソッドが Rows オブジェクトを返す際、s.db.addDep(s, rows) が呼び出されます。これは、「Stmt は、自身が生成したこの Rows オブジェクトがアクティブである間はクローズされてはならない」という依存関係を明示的に登録します。
    • Rows.Close() での依存関係の解消: Rows.Close() メソッドが呼び出されると、s.db.removeDep(s, rows) が呼び出されます。これにより、Stmt から Rows への依存関係が解消され、Stmt の参照カウントが適切に減少します。
    • Stmt.Close() の新しい振る舞い: 以前は Stmt.Close() が呼び出されると、すぐに基盤となるドライバステートメントがクローズされる可能性がありました。しかし、新しい実装では、Stmt.Close() は直接ドライバステートメントをクローズしません。代わりに、s.db.removeDep(s, s) を呼び出し、Stmt 自体に対する依存関係を解消しようとします。
      • もし Stmt がまだ他のオブジェクト(例えば、アクティブな Rows)に依存されている場合、removeDepStmt.finalClose() を呼び出しません。
      • Stmt.finalClose() は、Stmt がもはやどこからも参照されていない(依存されていない)場合にのみ実行されます。
    • Stmt.finalClose()noteUnusedDriverStatement: Stmt.finalClose() が実行されると、noteUnusedDriverStatement を介して、基盤となるドライバステートメントのクローズがスケジュールされます。このスケジュールは、関連する接続 (driver.Conn) が接続プールに戻されたときに実行されるように設定されます。これにより、Rows がクローズされて接続がプールに戻されるまで、ドライバステートメントが安全に保持されることが保証されます。
  5. driver.Stmt.Close の契約の簡素化:

    • このコミット以前は、driver.Stmt.Close のドキュメントには「ステートメントをクローズしても、そのステートメントから作成された未処理のクエリを中断してはならない」という複雑な要件がありました。これはドライバの実装者にとって負担でした。
    • このコミットにより、database/sql パッケージが内部で参照カウンティングと依存関係管理を行うようになったため、ドライバの実装者はこの複雑な要件を考慮する必要がなくなりました。新しい契約では、「Go 1.1以降、Stmt は、いかなるクエリによっても使用されている間はクローズされません」と簡素化されました。これにより、ドライバの実装が容易になり、エコシステム全体の健全性が向上します。

これらの変更により、database/sql パッケージは、StmtRows の間の複雑なライフタイム依存関係を透過的に管理できるようになり、リソースリークや競合状態を防ぎ、アプリケーションの堅牢性を大幅に向上させました。

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

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

driver.Stmt インターフェースの Close メソッドのコメントが変更され、その契約が簡素化されました。

--- a/src/pkg/database/sql/driver/driver.go
+++ b/src/pkg/database/sql/driver/driver.go
@@ -115,23 +115,8 @@ type Result interface {
 type Stmt interface {
 	// Close closes the statement.
 	//
-	// Closing a statement should not interrupt any outstanding
-	// query created from that statement. That is, the following
-	// order of operations is valid:
-	//
-	//  * create a driver statement
-	//  * call Query on statement, returning Rows
-	//  * close the statement
-	//  * read from Rows
-	//
-	// If closing a statement invalidates currently-running
-	// queries, the final step above will incorrectly fail.
-	//
-	// TODO(bradfitz): possibly remove the restriction above, if
-	// enough driver authors object and find it complicates their
-	// code too much. The sql package could be smarter about
-	// refcounting the statement and closing it at the appropriate
-	// time.
+	// As of Go 1.1, a Stmt will not be closed if it's in use
+	// by any queries.
 	Close() error
 
 	// NumInput returns the number of placeholder parameters.

src/pkg/database/sql/sql.go

このファイルには多くの変更が含まれていますが、主要な部分を抜粋します。

  • DB 構造体への新しいフィールドの追加: 参照カウンティングと接続管理のためのマップが追加されました。

    type DB struct {
        // ... 既存のフィールド ...
        mu        sync.Mutex           // protects following fields
        outConn   map[driver.Conn]bool // whether the conn is in use
        freeConn  []driver.Conn
        closed    bool
        dep       map[finalCloser]depSet
        onConnPut map[driver.Conn][]func() // code (with mu held) run when conn is next returned
        lastPut   map[driver.Conn]string   // stacktrace of last conn's put; debug only
    }
    
  • finalCloser インターフェースと depSet 型の定義: 依存関係管理の基盤となるインターフェースと型が定義されました。

    type depSet map[interface{}]bool // set of true bools
    
    type finalCloser interface {
        // finalClose is called when the reference count of an object
        // goes to zero. (*DB).mu is not held while calling it.
        finalClose() error
    }
    
  • addDep および removeDep メソッドの追加: オブジェクト間の依存関係を追加・削除し、参照カウントがゼロになった場合に finalClose を呼び出すロジックです。

    // addDep notes that x now depends on dep, and x's finalClose won't be
    // called until all of x's dependencies are removed with removeDep.
    func (db *DB) addDep(x finalCloser, dep interface{}) {
        db.mu.Lock()
        defer db.mu.Unlock()
        if db.dep == nil {
            db.dep = make(map[finalCloser]depSet)
        }
        xdep := db.dep[x]
        if xdep == nil {
            xdep = make(depSet)
            db.dep[x] = xdep
        }
        xdep[dep] = true
    }
    
    // removeDep notes that x no longer depends on dep.
    // If x still has dependencies, nil is returned.
    // If x no longer has any dependencies, its finalClose method will be
    // called and its error value will be returned.
    func (db *DB) removeDep(x finalCloser, dep interface{}) error {
        done := false
    
        db.mu.Lock()
        xdep := db.dep[x]
        if xdep != nil {
            delete(xdep, dep)
            if len(xdep) == 0 {
                delete(db.dep, x)
                done = true
            }
        }
        db.mu.Unlock()
    
        if !done {
            return nil
        }
        return x.finalClose()
    }
    
  • putConn メソッドの修正: 接続がプールに戻される際に、onConnPut に登録されたクリーンアップ関数を実行するロジックが追加されました。

    func (db *DB) putConn(c driver.Conn, err error) {
        db.mu.Lock()
        if !db.outConn[c] {
            // ... (debugGetPut 関連のパニック処理) ...
        }
        delete(db.outConn, c) // 接続を使用中リストから削除
    
        if fns, ok := db.onConnPut[c]; ok { // onConnPut に登録された関数があれば実行
            for _, fn := range fns {
                fn()
            }
            delete(db.onConnPut, c)
        }
    
        if err == driver.ErrBadConn {
            db.mu.Unlock()
            return
        }
        // ... (既存のプールに戻すロジック) ...
    }
    
  • prepare メソッドの修正: Stmt 作成時に Stmt 自体を依存関係として登録します。

    func (db *DB) prepare(query string) (*Stmt, error) {
        // ... (既存の接続取得ロジック) ...
        stmt := &Stmt{
            db:    db,
            query: query,
            css:   []connStmt{{ci, si}},
        }
        db.addDep(stmt, stmt) // Stmt自身を依存関係として登録
        return stmt, nil
    }
    
  • Stmt 構造体への closemu の追加と Exec, Query メソッドでのロックの利用: Stmt のクローズ処理中の競合を防ぐためのRWMutexが追加され、ExecQuery で読み取りロックが使用されます。

    type Stmt struct {
        // ...
        closemu sync.RWMutex // held exclusively during close, for read otherwise.
    }
    func (s *Stmt) Exec(args ...interface{}) (Result, error) {
        s.closemu.RLock()
        defer s.closemu.RUnlock()
        // ...
    }
    func (s *Stmt) Query(args ...interface{}) (*Rows, error) {
        s.closemu.RLock()
        defer s.closemu.RUnlock()
        // ...
    }
    
  • Stmt.Query メソッドでの StmtRows の依存関係の追加: Stmt が生成した Rows に依存することを登録し、Rows クローズ時に依存関係を解消するコールバックを設定します。

    func (s *Stmt) Query(args ...interface{}) (*Rows, error) {
        // ...
        rows := &Rows{
            db:    s.db,
            ci:    ci,
            rowsi: rowsi,
            // releaseConn set below
        }
        s.db.addDep(s, rows) // StmtがRowsに依存することを登録
        rows.releaseConn = func(err error) {
            releaseConn(err)
            s.db.removeDep(s, rows) // Rowsクローズ時に依存関係を解消
        }
        return rows, nil
    }
    
  • Stmt.Close メソッドの修正と Stmt.finalClose メソッドの追加: Stmt.Close は直接クローズせず、依存関係管理システムに委ねるようになりました。実際のクリーンアップは finalClose で行われます。

    func (s *Stmt) Close() error {
        s.closemu.Lock()
        defer s.closemu.Unlock()
    
        if s.stickyErr != nil {
            return s.stickyErr
        }
        if s.tx != nil { // トランザクション内のStmtはTxが管理
            s.txsi.Close()
            return nil
        }
        return s.db.removeDep(s, s) // Stmt自身の依存関係を解消
    }
    
    func (s *Stmt) finalClose() error {
        for _, v := range s.css {
            s.db.noteUnusedDriverStatement(v.ci, v.si) // ドライバステートメントのクローズをスケジュール
        }
        s.css = nil // 内部のドライバステートメント参照をクリア
        return nil
    }
    
  • noteUnusedDriverStatement 関数の追加: ドライバステートメントのクローズを、関連する接続がプールに戻されたときに実行されるようにスケジュールします。

    // noteUnusedDriverStatement notes that si is no longer used and should
    // be closed whenever possible (when c is next not in use), unless c is
    // already closed.
    func (db *DB) noteUnusedDriverStatement(c driver.Conn, si driver.Stmt) {
        db.mu.Lock()
        defer db.mu.Unlock()
        if db.outConn[c] { // 接続がまだ使用中の場合
            db.onConnPut[c] = append(db.onConnPut[c], func() {
                si.Close() // 接続がプールに戻されたときにクローズ
            })
        } else {
            si.Close() // 接続が使用中でない場合、すぐにクローズ
        }
    }
    

src/pkg/database/sql/sql_test.go

TestCloseStmtBeforeRows テストが有効化され、Issue 3865が修正されたことを確認できるようになりました。

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -8,7 +8,6 @@ import (
 	"database/sql/driver"
 	"fmt"
 	"reflect"
-	"runtime"
 	"strings"
 	"testing"
 	"time"
@@ -448,10 +451,8 @@ func TestIssue2542Deadlock(t *testing.T) {
 	}
 }
 
+// From golang.org/issue/3865
 func TestCloseStmtBeforeRows(t *testing.T) {
-	t.Skip("known broken test; golang.org/issue/3865")
-	return
-
 	db := newTestDB(t, "people")
 	defer closeDB(t, db)
 

コアとなるコードの解説

このコミットは、database/sql パッケージにおける StmtRows の間の複雑なライフタイム依存関係を、参照カウンティングとコールバックメカニズムを用いて解決しています。

  1. DB 構造体の役割強化: DB 構造体は、単なるデータベース接続のプールだけでなく、StmtRows といったリソースのライフタイムを管理する中央ハブとしての役割を担うようになりました。

    • outConn マップは、現在アプリケーションによって使用されている(プールから取り出されている)接続を正確に追跡します。これにより、putConn で接続が誤って二重にプールに戻されるのを防ぎ、接続の状態管理を厳密にします。
    • dep マップは、finalCloser インターフェースを実装するオブジェクト間の依存関係を記録します。例えば、StmtRows を生成した場合、Stmt はその Rows に依存するとマークされます。このメカニズムにより、Rows がクローズされるまで StmtfinalClose が呼び出されないことが保証され、使用中のリソースが誤って解放されることを防ぎます。
    • onConnPut は、特定の接続がプールに戻されたときに実行されるべき遅延クリーンアップ操作(例: ドライバのステートメントのクローズ)をキューイングするために使用されます。これにより、接続が安全に再利用可能になった時点で、その接続に関連するリソースを解放できます。
  2. finalCloser と依存関係管理:

    • finalCloser インターフェースは、オブジェクトが完全に解放される直前に行うべきクリーンアップ処理を抽象化します。Stmt はこのインターフェースを実装し、その finalClose メソッドで基盤となるドライバステートメントをクローズするロジックを含みます。
    • addDepremoveDep は、この参照カウンティングメカニズムの核心です。addDep は依存関係を追加し、removeDep は依存関係を削除します。removeDep が呼び出され、かつそのオブジェクトの依存関係がすべて解消された場合(つまり、参照カウントがゼロになった場合)、finalClose が呼び出されます。この設計により、リソースの安全かつ適切なタイミングでの解放が保証されます。
  3. Stmt.Close() の新しい振る舞い:

    • 以前は Stmt.Close() が呼び出されると、すぐに基盤となるドライバステートメントがクローズされる可能性がありました。しかし、Stmt から生成された Rows がまだアクティブな場合、これは問題を引き起こしました。
    • 新しい実装では、Stmt.Close() は直接ドライバステートメントをクローズしません。代わりに、s.db.removeDep(s, s) を呼び出し、Stmt 自体に対する依存関係を解消しようとします。
    • もし Stmt がまだ他のオブジェクト(例えば、アクティブな Rows)に依存されている場合、removeDepStmt.finalClose() を呼び出しません。Stmt.finalClose() は、Stmt がもはやどこからも参照されていない(依存されていない)場合にのみ実行されます。
    • Stmt.finalClose() が実行されると、noteUnusedDriverStatement を介して、基盤となるドライバステートメントのクローズがスケジュールされます。このスケジュールは、関連する接続がプールに戻されたときに実行されるように設定されます。これにより、Rows がクローズされて接続がプールに戻されるまで、ドライバステートメントが安全に保持されることが保証されます。
  4. RowsStmt の連携:

    • Stmt.Query()Rows を返す際、s.db.addDep(s, rows) を呼び出すことで、Stmt がこの Rows に依存することを登録します。これは、Rows がアクティブである間は Stmt をクローズしてはならないという明確な指示となります。
    • Rows.Close() が呼び出されると、s.db.removeDep(s, rows) を呼び出すことで、Stmt からの Rows への依存関係を解消します。これにより、Stmt の参照カウントが適切に管理され、Rows がクローズされた後に Stmt が安全にクローズされる準備が整います。

これらの変更により、database/sql パッケージは、StmtRows の間の複雑なライフタイム依存関係を透過的に管理できるようになり、ドライバ開発者は Stmt.Close の実装についてより単純な契約に従うことができるようになりました。これは、リソースリークや競合状態を防ぎ、アプリケーションの堅牢性を向上させる上で非常に重要です。

関連リンク

  • Go Issue 3865: database/sql: Stmt.Close should not close underlying driver.Stmt if Rows is still in use https://github.com/golang/go/issues/3865
  • Go Issue 4459: database/sql: DB.Close doesn't close all connections https://github.com/golang/go/issues/4459
  • Gerrit Change-Id: I7363043 (コミットメッセージに記載されている golang.org/cl/7363043 に対応するGoのコードレビューシステムのエントリ)

参考にした情報源リンク