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

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

このコミットは、Go言語の database/sql パッケージにおける重要なバグ修正と機能改善を目的としています。具体的には、driver.Conn (データベース接続) が、それに関連付けられた driver.Stmt (プリペアドステートメント) がまだ閉じられていない状態で閉じられてしまう問題を解決します。これにより、リソースリークや未定義の動作、さらにはクラッシュが発生する可能性がありました。この変更により、driver.Conn は、その接続に依存するすべての Stmt が閉じられるまで、実際の基盤となる接続を閉じないように制御されるようになります。

コミット

commit 209f6b1d2ca84541505c29c6158cde9d5a0bbd57
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Mar 25 16:50:27 2013 -0700

    database/sql: don't close a driver.Conn until its Stmts are closed
    
    Fixes #5046
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8016044

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

https://github.com/golang/go/commit/209f6b1d2ca84541505c29c6158cde9d5a0bbd57

元コミット内容

database/sql: driver.Conn は、その Stmt が閉じられるまで閉じないようにする。 Issue #5046 を修正。

変更の背景

この変更の背景には、Goの標準ライブラリである database/sql パッケージにおけるリソース管理の課題がありました。以前の実装では、DB.Close()DB.SetMaxIdleConns() などによってデータベース接続 (driver.Conn) が閉じられる際に、その接続から生成されたプリペアドステートメント (driver.Stmt) がまだアクティブであるかどうかを適切に考慮していませんでした。

具体的には、以下のような問題が発生する可能性がありました。

  1. リソースリーク: driver.Conn が閉じられた後も、それに関連付けられた driver.Stmt オブジェクトがメモリ上に残り、閉じられた接続を参照し続ける。これにより、データベースドライバによっては、基盤となるデータベースリソースが適切に解放されない可能性がありました。
  2. 未定義の動作/クラッシュ: 閉じられた接続を参照している driver.Stmt が後続の操作(例: Stmt.Query(), Stmt.Exec()) を試みると、無効な接続に対して操作を行おうとするため、パニック、セグメンテーション違反、またはその他の予測不能なエラーが発生する可能性がありました。これは特に、接続プールが接続を再利用する際に顕著になる可能性があります。
  3. 競合状態: 複数のゴルーチンが同じ driver.Conn から生成された driver.Stmt を使用している場合、driver.Conn が予期せず閉じられると、競合状態やデッドロックを引き起こす可能性がありました。

Issue #5046 (database/sql: must call (*driver.Stmt).Close before correspondent (*driver.Conn).Close) は、この問題を明確に指摘しており、driver.Conn がその Stmt よりも先に閉じられるべきではないという要件を提起していました。このコミットは、この根本的な問題を解決し、database/sql パッケージのリソース管理をより堅牢にするために導入されました。

前提知識の解説

このコミットを理解するためには、Go言語の database/sql パッケージの基本的な概念と、一般的なデータベース接続管理の知識が必要です。

  1. database/sql パッケージ:

    • Goの標準ライブラリで、データベースにアクセスするための汎用的なインターフェースを提供します。特定のデータベースドライバに依存しない抽象化レイヤーです。
    • DB 構造体: データベースへの抽象的なアクセスポイントを表します。内部的に接続プールを管理し、必要に応じて接続を開閉します。
    • Conn (または driver.Conn): データベースへの単一の物理的な接続を表すインターフェースです。DB は複数の Conn をプールして管理します。
    • Stmt (または driver.Stmt): プリペアドステートメントを表します。SQLクエリを事前にデータベースに送信して準備しておくことで、繰り返し実行する際のパフォーマンスを向上させ、SQLインジェクション攻撃を防ぎます。Stmt は特定の Conn に関連付けられます。
    • Tx (または driver.Tx): データベーストランザクションを表します。複数のデータベース操作をアトミックに実行するために使用されます。
    • Close() メソッド: DB, Conn, Stmt, Rows など、多くのリソース型に存在するメソッドで、使用後にリソースを明示的に解放するために呼び出されます。Goでは、defer キーワードと組み合わせて使用されることが一般的です。
  2. リソース管理と依存関係:

    • プログラミングにおいて、ファイルハンドル、ネットワークソケット、データベース接続などのリソースは、使用後に適切に解放される必要があります。解放を怠ると、リソースリークが発生し、システムのパフォーマンス低下や不安定化につながります。
    • 特に、あるリソースが別のリソースに依存している場合(例: StmtConn に依存している)、依存元のリソースが解放される前に依存先のリソースが解放されると、問題が発生します。このコミットは、まさにこの依存関係の管理を改善するものです。
  3. sync.Mutex:

    • Goの標準ライブラリ sync パッケージに含まれるミューテックス(相互排他ロック)です。複数のゴルーチンが共有リソースに同時にアクセスするのを防ぎ、データ競合を防ぐために使用されます。このコミットでは、driverConnDB の内部状態を保護するために使用されています。
  4. finalCloser インターフェース:

    • このコミットで導入された内部的なインターフェースで、finalClose() error メソッドを持つ型を定義します。これは、依存関係がすべて解消された後に最終的に閉じられるべきリソースを抽象化するために使用されます。

技術的詳細

このコミットの核心は、database/sql パッケージにおける driver.Conndriver.Stmt の間の依存関係を明示的に追跡し、管理するメカニズムを導入した点にあります。これにより、driver.Conn が、それに関連付けられた driver.Stmt がすべて閉じられるまで、基盤となるデータベース接続を閉じないように保証されます。

主な技術的変更点は以下の通りです。

  1. driverConn 構造体の拡張:

    • src/pkg/database/sql/sql.go 内の driverConn 構造体に、以下のフィールドが追加されました。
      • db *DB: この接続が属する DB インスタンスへのポインタ。これにより、driverConn から DB の依存関係管理機能にアクセスできるようになります。
      • closed bool: この driverConn が論理的に閉じられた状態にあるかを示すフラグ。これにより、二重クローズを防ぎます。
    • driverConn には、closeDBLocked(), Close(), finalClose() という新しいメソッドが追加されました。
      • driverConn.Close(): 外部から呼び出されるクローズメソッド。これは、DB の依存関係管理システムを通じて、この driverConn が持つすべての依存関係(Stmtなど)が解放された後に、実際の基盤となる driver.ConnClose() メソッド (ci.Close()) が呼び出されるようにスケジュールします。
      • driverConn.finalClose(): 実際に基盤となる driver.Conn (ci) を閉じるメソッド。これは、DB の依存関係管理システムによって、この driverConn に依存するものがなくなったと判断された場合にのみ呼び出されます。
  2. DB 構造体における依存関係管理の強化:

    • DB 構造体には、dep map[finalCloser]depSet というマップが追加されました。これは、finalCloser インターフェースを実装するオブジェクト(例: driverConn, Stmt)が、他のどのオブジェクトに依存しているかを追跡するためのものです。
    • DB.addDep(x finalCloser, dep interface{}) および DB.removeDep(x finalCloser, dep interface{}) メソッドが大幅に改修されました。
      • addDep: xdep に依存していることを記録します。例えば、StmtdriverConn に依存している場合、db.addDep(dc, stmt) のように呼び出されます。
      • removeDep: xdep への依存関係を解消したことを記録します。Stmt が閉じられた際に db.removeDep(dc, stmt) のように呼び出されます。
      • removeDep は、x に依存するものがなくなった場合にのみ、x.finalClose() を呼び出すように変更されました。これにより、driverConn は、それに関連付けられたすべての Stmt が閉じられるまで、実際のクローズ処理を遅延させることができます。
  3. StmtdriverConn の間の依存関係の確立:

    • DB.prepare() メソッド内で Stmt が作成される際に、db.addDep(stmt, stmt) に加えて db.addDep(dc, stmt) が呼び出されるようになりました。これは、「この Stmt は、この driverConn に依存している」という関係を明示的に登録します。
    • Stmt.connStmt() メソッド(Stmt が接続を取得する内部メソッド)でも、s.db.addDep(dc, s) が呼び出され、同様の依存関係が登録されます。
    • Stmt.finalClose() メソッド(Stmt が最終的に閉じられる際に呼び出される)内で、s.db.removeDep(v.dc, s) が呼び出されるようになりました。これは、「この Stmt は、この driverConn への依存関係を解消した」ことを通知します。
  4. 接続クローズロジックの変更:

    • DB.Close() および DB.SetMaxIdleConns() 内で、アイドル状態の接続を閉じる際に、直接 dc.ci.Close() を呼び出すのではなく、新しく導入された dc.closeDBLocked() または dc.Close() を呼び出すように変更されました。これにより、接続のクローズ処理が依存関係管理システムを通じて行われるようになります。
    • DB.putConn() (使用済み接続をプールに戻す) でも、dc.Close() が呼び出されるようになりました。
  5. テストケースの追加:

    • src/pkg/database/sql/sql_test.goTestCloseConnBeforeStmts という新しいテストケースが追加されました。このテストは、DB.Prepare()Stmt を作成した後、DB.Close() を呼び出し、その後で Stmt.Close() を呼び出すというシナリオをシミュレートします。これにより、driver.ConnStmt よりも先に閉じられないことを検証し、このコミットによる修正が正しく機能していることを確認します。

これらの変更により、database/sql パッケージは、driver.Conndriver.Stmt の間のライフサイクル依存関係をより堅牢に管理できるようになり、リソースリークや不正な接続使用による問題を効果的に防止します。

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

このコミットにおける主要なコード変更は、src/pkg/database/sql/sql.go に集中しています。

  1. driverConn 構造体の変更 (src/pkg/database/sql/sql.go L204-207):

    type driverConn struct {
    	db *DB // 追加
    
    	sync.Mutex // guards following
    	ci         driver.Conn
    	closed     bool // 追加
    }
    
    • db *DB フィールドと closed bool フィールドが追加されました。
  2. driverConn の新しいメソッド (src/pkg/database/sql/sql.go L210-236):

    • closeDBLocked() error
    • Close() error
    • finalClose() error これらのメソッドが追加され、接続のクローズロジックと依存関係管理が実装されました。
  3. DB.addDep および DB.removeDep の変更 (src/pkg/database/sql/sql.go L238-310):

    • DB.addDepdb.addDepLocked を呼び出すように変更され、db.addDepLocked が追加されました。
    • DB.removeDepdb.removeDepLocked を呼び出すように変更され、db.removeDepLocked が追加されました。
    • これらのメソッドの内部ロジックが、依存関係の追跡と finalCloser の呼び出しを適切に行うように大幅に修正されました。特に、removeDepLocked は、依存関係がなくなった場合にのみ finalClose を呼び出す関数を返すようになりました。
  4. DB.Close() の変更 (src/pkg/database/sql/sql.go L320-326):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -320,9 +365,7 @@ func (db *DB) Close() error {
     	defer db.mu.Unlock()
     	var err error
     	for _, dc := range db.freeConn {
    -		dc.Lock()
    -		err1 := dc.ci.Close()
    -		dc.Unlock()
    +		err1 := dc.closeDBLocked() // 変更
     		if err1 != nil {
     			err = err1
     		}
    
    • dc.ci.Close() の直接呼び出しが dc.closeDBLocked() に変更されました。
  5. DB.SetMaxIdleConns() の変更 (src/pkg/database/sql/sql.go L365-372):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -365,11 +408,7 @@ func (db *DB) SetMaxIdleConns(n int) {
     		dc := db.freeConn[nfree-1]
     		db.freeConn[nfree-1] = nil
     		db.freeConn = db.freeConn[:nfree-1]
    -		go func() {
    -			dc.Lock()
    -			dc.ci.Close()
    -			dc.Unlock()
    -		}()
    +		go dc.Close() // 変更
     	}
     }
    
    • dc.ci.Close() の直接呼び出しが go dc.Close() に変更されました。
  6. DB.conn() の変更 (src/pkg/database/sql/sql.go L393-402):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -393,8 +432,12 @@ func (db *DB) conn() (*driverConn, error) {
     	if err != nil {
     		return nil, err
     	}
    -	dc := &driverConn{ci: ci}
    +	dc := &driverConn{ // 変更
    +		db: db, // 追加
    +		ci: ci,
    +	}
     	db.mu.Lock()
    +	db.addDepLocked(dc, dc) // 追加
     	db.outConn[dc] = true
     	db.mu.Unlock()
     	return dc, nil
    
    • driverConn の初期化時に db フィールドが設定され、db.addDepLocked(dc, dc) が呼び出されるようになりました。
  7. DB.putConn() の変更 (src/pkg/database/sql/sql.go L484-489):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -484,9 +527,7 @@ func (db *DB) putConn(dc *driverConn, err error) {
     	// statements which are still active?
     	db.mu.Unlock()
     
    -	dc.Lock()
    -	dc.ci.Close()
    -	dc.Unlock()
    +	dc.Close() // 変更
     }
    
    • dc.ci.Close() の直接呼び出しが dc.Close() に変更されました。
  8. DB.prepare() の変更 (src/pkg/database/sql/sql.go L528-534):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -528,6 +569,7 @@ func (db *DB) prepare(query string) (*Stmt, error) {
     		css:   []connStmt{{dc, si}},
     	}
     	db.addDep(stmt, stmt)
    +	db.addDep(dc, stmt) // 追加
     	db.putConn(dc, nil)
     	return stmt, nil
     }
    
    • db.addDep(dc, stmt) が追加され、StmtdriverConn に依存することを登録します。
  9. Stmt.connStmt() の変更 (src/pkg/database/sql/sql.go L1031-1037):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -1031,6 +1073,7 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St
     		if err != nil {
     			return nil, nil, nil, err
     		}
    +		s.db.addDep(dc, s) // 追加
     		s.mu.Lock()
     		cs = connStmt{dc, si}
     		s.css = append(s.css, cs)
    
    • s.db.addDep(dc, s) が追加され、StmtdriverConn に依存することを登録します。
  10. Stmt.finalClose() の変更 (src/pkg/database/sql/sql.go L1149-1154):

    --- a/src/pkg/database/sql/sql.go
    +++ b/src/pkg/database/sql/sql.go
    @@ -1149,6 +1192,7 @@ func (s *Stmt) Close() error {
     func (s *Stmt) finalClose() error {
     	for _, v := range s.css {
     		s.db.noteUnusedDriverStatement(v.dc, v.si)
    +		s.db.removeDep(v.dc, s) // 追加
     	}
     	s.css = nil
     	return nil
    
    • s.db.removeDep(v.dc, s) が追加され、StmtdriverConn への依存関係を解消したことを通知します。
  11. テストファイルの変更 (src/pkg/database/sql/fakedb_test.gosrc/pkg/database/sql/sql_test.go):

    • fakedb_test.gohookPostCloseConn が追加され、fakeConn のクローズをフックできるようになりました。
    • sql_test.goTestCloseConnBeforeStmts が追加され、このコミットの修正を検証するテストケースが実装されました。

コアとなるコードの解説

このコミットのコアとなる変更は、database/sql パッケージがデータベース接続 (driver.Conn) とプリペアドステートメント (driver.Stmt) の間のライフサイクル依存関係をどのように管理するかを根本的に変えるものです。

  1. driverConn の強化と finalClose メカニズム:

    • 以前の driverConn は単に基盤となる driver.Conn インターフェースをラップするだけでした。この変更により、driverConn は自身の DB インスタンスへの参照と、自身が論理的に閉じられたかどうかを示す closed フラグを持つようになりました。
    • 最も重要なのは、driverConn.Close()driverConn.finalClose() の導入です。
      • driverConn.Close() は、外部から接続を閉じようとする際に呼び出されます。しかし、このメソッドはすぐに基盤となる接続を閉じるわけではありません。代わりに、DB の依存関係管理システムに、この driverConn を閉じる準備ができたことを通知します。
      • driverConn.finalClose() は、実際に基盤となる driver.Conn (ci) の Close() メソッドを呼び出し、リソースを解放する役割を担います。このメソッドは、DB の依存関係管理システムによって、この driverConn に依存するすべての Stmt が閉じられたと判断された場合にのみ呼び出されます。これにより、driver.Conn がアクティブな Stmt を持つ状態で閉じられることがなくなります。
  2. DB における依存関係追跡の導入:

    • DB 構造体に追加された dep マップは、finalCloser インターフェースを実装するオブジェクト(driverConnStmt など)間の依存関係を追跡するための中心的な役割を果たします。
    • DB.addDep(x, dep) は、「xdep に依存している」という関係を登録します。例えば、DB.prepare()Stmt が作成される際に db.addDep(dc, stmt) が呼び出されます。これは、「この Stmt は、この driverConn に依存している」ことを意味します。
    • DB.removeDep(x, dep) は、「xdep への依存関係を解消した」ことを通知します。例えば、Stmt.Close() が呼び出される際に db.removeDep(v.dc, s) が呼び出されます。
    • removeDep のロジックは、x に依存するものがなくなった場合にのみ、x.finalClose() を呼び出すように設計されています。これにより、driverConn は、それに依存するすべての Stmt が閉じられるまで、実際のクローズ処理を「保留」することができます。
  3. StmtdriverConn のライフサイクル連携:

    • DB.prepare()Stmt.connStmt()Stmt が作成されるたびに、その Stmt が使用する driverConn に対する依存関係が db.addDep(dc, stmt) を通じて登録されます。
    • Stmt.Close() が呼び出され、Stmt.finalClose() が実行されると、db.removeDep(v.dc, s) が呼び出され、driverConn への依存関係が解除されます。
    • この依存関係の登録と解除のメカニズムにより、DB は各 driverConn に関連付けられたアクティブな Stmt の数を正確に把握できます。driverConn に依存する Stmt が一つもなくなったときに初めて、driverConn.finalClose() が呼び出され、基盤となるデータベース接続が安全に閉じられます。

これらの変更は、database/sql パッケージがデータベース接続をより堅牢かつ安全に管理するための基盤を築きました。これにより、開発者は接続とステートメントのライフサイクルについて過度に心配することなく、データベース操作に集中できるようになります。

関連リンク

参考にした情報源リンク