[インデックス 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 usehttps://github.com/golang/go/issues/3865 - Go Issue 4459:
database/sql: DB.Close doesn't close all connectionshttps://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