[インデックス 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
オブジェクトがまだ使用中であるにもかかわらず、基盤となるデータベースドライバのステートメントが誤ってクローズされてしまう可能性がありました。
この問題は、以下のようなシナリオで顕在化しました。
DB.Prepare()
でStmt
を作成。Stmt.Query()
でRows
を取得。Stmt.Close()
を呼び出す。Rows
からデータを読み取ろうとするが、基盤のステートメントが既にクローズされているためエラーが発生する。
このような状況は、リソースリークや、使用中のリソースが予期せず解放されることによる競合状態、アプリケーションのクラッシュを引き起こす可能性がありました。database/sql
パッケージは、複数のゴルーチンから安全に利用されることを前提としているため、このようなライフタイムの競合は深刻な問題でした。
このコミットの目的は、Stmt
と Rows
の間の依存関係を適切に管理するための堅牢な参照カウンティングメカニズムを導入し、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リソースは、明示的なクローズ操作が必要です。これらのリソースはガベージコレクタの管理外にあるため、プログラマが適切に解放する必要があります。しかし、複数のオブジェクトが同じ基盤リソースに依存している場合、いつそのリソースを安全にクローズできるかを判断するのは複雑になります。このような場合に、参照カウンティングの概念を適用することで、依存関係がすべて解消されたときにのみリソースを解放する、という安全なメカニズムを構築できます。
このコミットでは、Stmt
が Rows
を生成し、その Rows
がアクティブである間は Stmt
の基盤となるドライバステートメントをクローズしてはならない、という依存関係を管理するために、参照カウンティングに似たメカニズムが導入されています。
技術的詳細
このコミットの主要な目的は、database/sql
パッケージにおける Stmt
と Rows
のライフタイム管理の堅牢性を向上させることです。これを実現するために、以下の技術的詳細が導入・変更されました。
-
DB
構造体の拡張とライフタイム管理の中央化:DB
構造体は、単なる接続プールとしてだけでなく、Stmt
やRows
といったリソースのライフタイムを管理する中心的な役割を担うようになりました。outConn map[driver.Conn]bool
: 現在アプリケーションによって使用中(プールから取り出されている)のdriver.Conn
を追跡します。これにより、接続が誤って二重にプールに戻されるのを防ぎ、接続の正確な状態を把握できます。dep map[finalCloser]depSet
:finalCloser
インターフェースを実装するオブジェクト間の依存関係を追跡するためのマップです。x
がdep
に依存している場合、x
のfinalClose
メソッドはdep
が解放されるまで呼び出されません。これは、Stmt
が生成したRows
がアクティブである間はStmt
をクローズしない、というロジックの基盤となります。onConnPut map[driver.Conn][]func()
: 特定のdriver.Conn
が接続プールに戻される際に実行されるべきクリーンアップ関数(例: ドライバのステートメントのクローズ)をキューイングするために使用されます。これにより、接続が安全に再利用可能になった時点で、その接続に関連するリソースを解放できます。lastPut map[driver.Conn]string
: デバッグ目的で、接続が最後にプールに戻された際のスタックトレースを記録します。これは、putConn
で二重に接続が戻されるなどの異常な状況を診断するのに役立ちます。
-
finalCloser
インターフェースとdepSet
型の導入:type finalCloser interface { finalClose() error }
: このインターフェースは、オブジェクトの参照カウントがゼロになり、完全に解放される直前に行うべきクリーンアップ処理を抽象化します。Stmt
型がこのインターフェースを実装し、そのfinalClose
メソッドで基盤となるドライバステートメントをクローズするロジックを含みます。type depSet map[interface{}]bool
:finalCloser
オブジェクトが依存している他のオブジェクトのセットを表すために使用される型です。
-
依存関係管理メカニズム (
addDep
,removeDep
):(*DB).addDep(x finalCloser, dep interface{})
:x
がdep
に依存していることをdb.dep
マップに記録します。これにより、x
のfinalClose
がdep
が解放される前に呼び出されるのを防ぎます。(*DB).removeDep(x finalCloser, dep interface{}) error
:x
がdep
への依存を解消したことをdb.dep
マップから削除します。もしx
のすべての依存関係が解消された場合(つまり、x
の参照カウントがゼロになった場合)、x.finalClose()
が呼び出され、その結果が返されます。このメカニズムにより、リソースの安全な解放が保証されます。
-
Stmt
とRows
のライフタイム管理の変更: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
)に依存されている場合、removeDep
はStmt.finalClose()
を呼び出しません。 Stmt.finalClose()
は、Stmt
がもはやどこからも参照されていない(依存されていない)場合にのみ実行されます。
- もし
Stmt.finalClose()
とnoteUnusedDriverStatement
:Stmt.finalClose()
が実行されると、noteUnusedDriverStatement
を介して、基盤となるドライバステートメントのクローズがスケジュールされます。このスケジュールは、関連する接続 (driver.Conn
) が接続プールに戻されたときに実行されるように設定されます。これにより、Rows
がクローズされて接続がプールに戻されるまで、ドライバステートメントが安全に保持されることが保証されます。
-
driver.Stmt.Close
の契約の簡素化:- このコミット以前は、
driver.Stmt.Close
のドキュメントには「ステートメントをクローズしても、そのステートメントから作成された未処理のクエリを中断してはならない」という複雑な要件がありました。これはドライバの実装者にとって負担でした。 - このコミットにより、
database/sql
パッケージが内部で参照カウンティングと依存関係管理を行うようになったため、ドライバの実装者はこの複雑な要件を考慮する必要がなくなりました。新しい契約では、「Go 1.1以降、Stmt
は、いかなるクエリによっても使用されている間はクローズされません」と簡素化されました。これにより、ドライバの実装が容易になり、エコシステム全体の健全性が向上します。
- このコミット以前は、
これらの変更により、database/sql
パッケージは、Stmt
と Rows
の間の複雑なライフタイム依存関係を透過的に管理できるようになり、リソースリークや競合状態を防ぎ、アプリケーションの堅牢性を大幅に向上させました。
コアとなるコードの変更箇所
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が追加され、Exec
とQuery
で読み取りロックが使用されます。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
メソッドでのStmt
とRows
の依存関係の追加: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
パッケージにおける Stmt
と Rows
の間の複雑なライフタイム依存関係を、参照カウンティングとコールバックメカニズムを用いて解決しています。
-
DB
構造体の役割強化:DB
構造体は、単なるデータベース接続のプールだけでなく、Stmt
やRows
といったリソースのライフタイムを管理する中央ハブとしての役割を担うようになりました。outConn
マップは、現在アプリケーションによって使用されている(プールから取り出されている)接続を正確に追跡します。これにより、putConn
で接続が誤って二重にプールに戻されるのを防ぎ、接続の状態管理を厳密にします。dep
マップは、finalCloser
インターフェースを実装するオブジェクト間の依存関係を記録します。例えば、Stmt
がRows
を生成した場合、Stmt
はそのRows
に依存するとマークされます。このメカニズムにより、Rows
がクローズされるまでStmt
のfinalClose
が呼び出されないことが保証され、使用中のリソースが誤って解放されることを防ぎます。onConnPut
は、特定の接続がプールに戻されたときに実行されるべき遅延クリーンアップ操作(例: ドライバのステートメントのクローズ)をキューイングするために使用されます。これにより、接続が安全に再利用可能になった時点で、その接続に関連するリソースを解放できます。
-
finalCloser
と依存関係管理:finalCloser
インターフェースは、オブジェクトが完全に解放される直前に行うべきクリーンアップ処理を抽象化します。Stmt
はこのインターフェースを実装し、そのfinalClose
メソッドで基盤となるドライバステートメントをクローズするロジックを含みます。addDep
とremoveDep
は、この参照カウンティングメカニズムの核心です。addDep
は依存関係を追加し、removeDep
は依存関係を削除します。removeDep
が呼び出され、かつそのオブジェクトの依存関係がすべて解消された場合(つまり、参照カウントがゼロになった場合)、finalClose
が呼び出されます。この設計により、リソースの安全かつ適切なタイミングでの解放が保証されます。
-
Stmt.Close()
の新しい振る舞い:- 以前は
Stmt.Close()
が呼び出されると、すぐに基盤となるドライバステートメントがクローズされる可能性がありました。しかし、Stmt
から生成されたRows
がまだアクティブな場合、これは問題を引き起こしました。 - 新しい実装では、
Stmt.Close()
は直接ドライバステートメントをクローズしません。代わりに、s.db.removeDep(s, s)
を呼び出し、Stmt
自体に対する依存関係を解消しようとします。 - もし
Stmt
がまだ他のオブジェクト(例えば、アクティブなRows
)に依存されている場合、removeDep
はStmt.finalClose()
を呼び出しません。Stmt.finalClose()
は、Stmt
がもはやどこからも参照されていない(依存されていない)場合にのみ実行されます。 Stmt.finalClose()
が実行されると、noteUnusedDriverStatement
を介して、基盤となるドライバステートメントのクローズがスケジュールされます。このスケジュールは、関連する接続がプールに戻されたときに実行されるように設定されます。これにより、Rows
がクローズされて接続がプールに戻されるまで、ドライバステートメントが安全に保持されることが保証されます。
- 以前は
-
Rows
とStmt
の連携: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
パッケージは、Stmt
と Rows
の間の複雑なライフタイム依存関係を透過的に管理できるようになり、ドライバ開発者は 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のコードレビューシステムのエントリ)
参考にした情報源リンク
- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
database/sql/driver
パッケージのドキュメント: https://pkg.go.dev/database/sql/driver - Go
sync
パッケージのドキュメント (Mutexの理解のため): https://pkg.go.dev/sync - Go
runtime
パッケージのドキュメント (stack関数やファイナライザの背景理解のため): https://pkg.go.dev/runtime