[インデックス 15777] ファイルの概要
コミット
commit f28c8fba67008ae0b14517979e9b48ac38fa22b6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 14 15:01:45 2013 -0700
database/sql: associate a mutex with each driver interface
The database/sql/driver docs make this promise:
"Conn is a connection to a database. It is not used
concurrently by multiple goroutines."
That promises exists as part of database/sql's overall
goal of making drivers relatively easy to write.
So far this promise has been kept without the use of locks by
being careful in the database/sql package, but sometimes too
careful. (cf. golang.org/issue/3857)
The CL associates a Mutex with each driver.Conn, and with the
interface value progeny thereof. (e.g. each driver.Tx,
driver.Stmt, driver.Rows, driver.Result, etc) Then whenever
those interface values are used, the Locker is locked.
This CL should be a no-op (aside from some new Lock/Unlock
pairs) and doesn't attempt to fix Issue 3857 or Issue 4459,
but should make it much easier in a subsequent CL.
Update #3857
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7803043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f28c8fba67008ae0b14517979e9b48ac38fa22b6
元コミット内容
database/sql: 各ドライバインターフェースにミューテックスを関連付ける
database/sql/driver のドキュメントには以下の約束があります:
「Conn はデータベースへの接続です。複数のゴルーチンによって同時に使用されることはありません。」
この約束は、ドライバを比較的簡単に記述できるようにするという database/sql の全体的な目標の一部として存在します。
これまでこの約束は、database/sql パッケージ内で注意深く実装することでロックを使用せずに守られてきましたが、時には注意しすぎることもありました。(参照: golang.org/issue/3857)
この変更リスト (CL) は、各 driver.Conn およびその派生インターフェース値 (例: 各 driver.Tx, driver.Stmt, driver.Rows, driver.Result など) に Mutex を関連付けます。これらのインターフェース値が使用されるたびに、Locker がロックされます。
この CL は (いくつかの新しい Lock/Unlock ペアを除いて) 何も変更しないはずであり、Issue 3857 や Issue 4459 を直接修正しようとはしていませんが、後続の CL での修正をはるかに容易にするはずです。
Issue #3857 を更新
R=golang-dev, adg CC=golang-dev https://golang.org/cl/7803043
変更の背景
Go言語の database/sql パッケージは、データベースドライバとアプリケーションコードの間の抽象化レイヤーを提供します。このパッケージの設計原則の一つに、ドライバの実装を簡素化するために、driver.Conn インターフェースが複数のゴルーチンによって同時に使用されないという保証があります。しかし、この保証を database/sql パッケージ側で厳密に守ろうとすると、過剰な注意や複雑なロジックが必要になる場合がありました。
特に、golang.org/issue/3857 では、database/sql パッケージが driver.Conn の非同時使用の約束をどのように扱うかについて議論されていました。この問題は、database/sql が内部的に接続をプールし、複数の操作で同じ接続を再利用する際に、ドライバの実装がこの非同時使用の約束に依存している場合に、予期せぬ競合状態やデッドロックが発生する可能性を示唆していました。
このコミットの目的は、database/sql パッケージがドライバインターフェースの非同時使用の約束をより堅牢に、かつシンプルに強制できるようにするための基盤を構築することです。具体的には、各ドライバインターフェース値(Connだけでなく、Tx、Stmt、Rows、Resultなど、Connから派生するすべてのインターフェース)に明示的にミューテックスを関連付けることで、これらのインターフェースへのアクセスを同期させ、ドライバが非同時使用の約束に安心して依存できるようにします。
この変更自体は、既存のバグ(Issue 3857やIssue 4459)を直接修正するものではありませんが、これらの問題を解決するための後続の変更を容易にすることを意図しています。これにより、database/sql パッケージの内部実装が簡素化され、将来的な改善やバグ修正がより安全かつ効率的に行えるようになります。
前提知識の解説
Go言語の database/sql パッケージ
Go言語の標準ライブラリに含まれる database/sql パッケージは、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作は database/sql/driver パッケージで定義されたインターフェースを実装する外部ドライバに委譲されます。
database/sql/driver パッケージ
database/sql/driver パッケージは、データベースドライバが実装すべきインターフェースを定義しています。主要なインターフェースには以下のようなものがあります。
driver.Driver: データベースへの接続を開くためのインターフェース。driver.Conn: データベースへの単一の接続を表すインターフェース。このインターフェースのドキュメントには「複数のゴルーチンによって同時に使用されない」という重要な約束があります。driver.Tx: トランザクションを表すインターフェース。driver.Stmt: プリペアドステートメントを表すインターフェース。driver.Rows: クエリ結果の行を表すインターフェース。driver.Result:Exec操作の結果を表すインターフェース。
ゴルーチンと並行性
Go言語は軽量なスレッドであるゴルーチン(goroutine)とチャネル(channel)を用いた並行処理をサポートしています。複数のゴルーチンが同時に実行される可能性があるため、共有リソースへのアクセスを適切に同期しないと、競合状態(race condition)が発生し、プログラムの動作が予測不能になったり、データが破損したりする可能性があります。
ミューテックス (sync.Mutex)
ミューテックス(Mutex)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。Go言語の sync パッケージには sync.Mutex が提供されており、Lock() メソッドでロックを取得し、Unlock() メソッドでロックを解放します。これにより、一度に一つのゴルーチンのみが保護されたコードセクション(クリティカルセクション)を実行することを保証し、競合状態を防ぎます。
インターフェースと埋め込み
Go言語のインターフェースは、メソッドのセットを定義します。構造体は、そのインターフェースのすべてのメソッドを実装することで、そのインターフェースを満たすことができます。また、Goの構造体は他の構造体やインターフェースを「埋め込む」ことができます。これにより、埋め込まれた型のメソッドが外側の型に「昇格」され、あたかも外側の型自身のメソッドであるかのように呼び出すことができます。このコミットでは、sync.Mutex を構造体に埋め込むことで、その構造体が sync.Locker インターフェースを満たすようにしています。
golang.org/issue/3857
このIssueは、database/sql パッケージにおける接続のライフサイクルと並行性に関する問題点を指摘しています。特に、driver.Conn が非同時使用であるという約束にもかかわらず、database/sql パッケージが内部的に接続を再利用する際に、ドライバの実装がこの約束に違反しているかのように見える状況が発生していました。このコミットは、この問題の根本的な解決策ではなく、その解決を容易にするための準備段階と位置づけられています。
技術的詳細
このコミットの核心は、database/sql パッケージが driver.Conn インターフェースとその派生インターフェース(driver.Tx, driver.Stmt, driver.Rows, driver.Result)へのアクセスを同期させるための新しいメカニズムを導入することです。
これまでの database/sql パッケージは、driver.Conn が非同時使用であるという約束を、内部的なロジックで「注意深く」守ろうとしていました。しかし、これは複雑さを増し、golang.org/issue/3857 のような問題を引き起こす可能性がありました。
このコミットでは、以下の新しい型が導入されます。
-
driverConn構造体:type driverConn struct { sync.Mutex ci driver.Conn }この構造体は、実際の
driver.Connインターフェース (ci) とsync.Mutexを埋め込んでいます。これにより、driverConnのインスタンスはsync.Lockerインターフェース(Lock()とUnlock()メソッドを持つ)を満たします。database/sqlパッケージは、データベースドライバから取得した生のdriver.Connを直接使用する代わりに、このdriverConnでラップして使用するようになります。これにより、driver.Connへのすべての操作の前にLock()を呼び出し、操作後にUnlock()を呼び出すことで、driver.Connへのアクセスを排他的に制御できるようになります。 -
driverStmt構造体:type driverStmt struct { sync.Locker // the *driverConn si driver.Stmt }この構造体は、
driver.Stmtインターフェース (si) と、そのdriver.Stmtが生成された元のdriverConnのsync.Lockerインターフェースを埋め込んでいます。これにより、driver.Stmtへの操作も、元の接続のミューテックスによって保護されるようになります。同様に、driver.Tx、driver.Rows、driver.Resultなども、対応するdriverConnのミューテックスによって保護されるように変更されています。
変更のメカニズム:
DB構造体の変更:DB構造体内の接続プール (outConn,freeConn,onConnPut,lastPut) が、driver.Connの代わりに新しく定義された*driverConnを保持するように変更されます。- 接続の取得と解放:
db.conn()メソッドは、生のdriver.Connを開いた後、それをdriverConnでラップして返します。db.putConn()メソッドも*driverConnを受け取るように変更されます。 - インターフェース操作の同期:
Exec,Query,Prepare,Begin,Commit,Rollbackなどの操作を行う際に、対応するdriverConnのLock()とUnlock()が呼び出されるようになります。これにより、driver.Connおよびその派生インターフェースへのすべての呼び出しが、ミューテックスによって保護されます。 withLockヘルパー関数: 新しいヘルパー関数withLockが導入され、ミューテックスのロックとアンロックを簡潔に記述できるようになります。func withLock(lk sync.Locker, fn func()) { lk.Lock() fn() lk.Unlock() }
この変更により、database/sql パッケージは、ドライバが driver.Conn の非同時使用の約束を破った場合でも、内部的に安全性を確保できるようになります。これは、ドライバの実装がこの約束に厳密に従っているかどうかに関わらず、database/sql パッケージが堅牢に動作するための重要なステップです。
コアとなるコードの変更箇所
主に src/pkg/database/sql/sql.go と src/pkg/database/sql/convert.go、そしてテストファイルである src/pkg/database/sql/sql_test.go が変更されています。
src/pkg/database/sql/sql.go
DB構造体のoutConn,freeConn,onConnPut,lastPutフィールドの型がdriver.Connから*driverConnに変更されました。- 新しい構造体
driverConnとdriverStmtが定義されました。driverConnはsync.Mutexとdriver.Connを持ちます。driverStmtはsync.Lockerとdriver.Stmtを持ちます。
db.conn()メソッドが*driverConnを返すように変更され、内部でdriver.ConnをdriverConnでラップするようになりました。db.putConn()メソッドが*driverConnを受け取るように変更されました。db.Close()メソッドで、freeConn内の接続を閉じる際にdc.Lock()とdc.Unlock()が追加されました。db.prepare(),db.exec(),db.queryConn(),db.begin()などのメソッドで、driver.Connの代わりに*driverConnを使用し、driver.Connインターフェースのメソッド呼び出しの前後でLock()とUnlock()が追加されました。Tx構造体のciフィールドがdriver.Connから*driverConnに変更され、Commit()とRollback()メソッドにtx.dc.Lock()とtx.dc.Unlock()が追加されました。Stmt構造体のconnStmtフィールドがdriver.Connから*driverConnに変更され、txsiフィールドがdriver.Stmtから*driverStmtに変更されました。resultFromStatement関数がdriver.Stmtの代わりにdriverStmtを受け取るように変更され、Exec呼び出しの前後でLock()とUnlock()が追加されました。rowsiFromStatement関数がdriver.Stmtの代わりにdriverStmtを受け取るように変更され、Query呼び出しの前後でLock()とUnlock()が追加されました。Rows構造体のciフィールドがdriver.Connから*driverConnに変更されました。- 新しいヘルパー関数
withLockが追加されました。
src/pkg/database/sql/convert.go
driverArgs関数がdriver.Stmtの代わりに*driverStmtを受け取るように変更されました。ColumnConverterのConvertValueメソッド呼び出しの前後でds.Lock()とds.Unlock()が追加されました。
src/pkg/database/sql/sql_test.go
- テストヘルパー関数
putConnHookの引数がdriver.Connから*driverConnに変更されました。 - テスト内の
db.freeConn[0]の型アサーションが*fakeConnからci.(*fakeConn)に変更され、driverConnの内部のdriver.Connにアクセスするように修正されました。
コアとなるコードの解説
このコミットの主要な変更は、database/sql パッケージがドライバインターフェースへのアクセスをどのように同期するかという点にあります。
-
driverConnとdriverStmtの導入:driverConnは、実際のdriver.Connインターフェースをsync.Mutexと共にラップします。これにより、database/sqlパッケージは、ドライバのConnインターフェースへのすべての呼び出しを、このMutexを使って保護できるようになります。driverStmtは、driver.Stmtインターフェースと、そのStmtが属するdriverConnのsync.Lockerを持ちます。これにより、Stmtへの操作も、元の接続のミューテックスによって保護されます。同様のラッパーがTx,Rows,Resultにも暗黙的に適用されます。
-
接続プールの変更:
DB構造体内の接続プール(outConn,freeConnなど)は、もはや生のdriver.Connを直接保持せず、*driverConnのポインタを保持するようになりました。これにより、プールから接続が取得されるたびに、その接続に関連付けられたミューテックスが利用可能になります。 -
ミューテックスによる保護の徹底:
database/sqlパッケージ内の、driver.Connやその派生インターフェース(driver.Tx,driver.Stmt,driver.Rows,driver.Result)のメソッドを呼び出すほぼすべての箇所で、対応するdriverConnのLock()とUnlock()が追加されました。 例えば、db.exec関数では、driver.ExecerインターフェースのExecメソッドを呼び出す前にdc.Lock()を行い、呼び出し後にdc.Unlock()を行っています。// 旧: // if execer, ok := ci.(driver.Execer); ok { // ... // resi, err := execer.Exec(query, dargs) // ... // } // 新: if execer, ok := dc.ci.(driver.Execer); ok { dargs, err := driverArgs(nil, args) if err != nil { return nil, err } dc.Lock() // ここでロック resi, err := execer.Exec(query, dargs) dc.Unlock() // ここでアンロック if err != driver.ErrSkip { if err != nil { return nil, err } return driverResult{dc, resi}, nil } }これにより、
database/sqlパッケージがドライバのインターフェースを呼び出す際には、常に排他的なアクセスが保証されます。 -
withLockヘルパー関数:withLock関数は、ミューテックスのロックとアンロックをdeferを使って安全に行うための簡潔な方法を提供します。これは、特にCloseメソッドのように、エラーが発生しても確実にアンロックする必要がある場合に便利です。func withLock(lk sync.Locker, fn func()) { lk.Lock() fn() lk.Unlock() }例えば、
sti.Close()の呼び出しがwithLockでラップされています。
この変更は、database/sql パッケージがドライバとのインタラクションにおいて、より堅牢な並行性制御を内部的に行うための重要なステップです。これにより、ドライバの実装が driver.Conn の非同時使用の約束を厳密に守っているかどうかにかかわらず、database/sql パッケージが安全に動作することが保証されます。これは、将来的なバグの修正や機能追加の基盤となります。
関連リンク
- Go言語
database/sqlパッケージ公式ドキュメント: https://pkg.go.dev/database/sql - Go言語
database/sql/driverパッケージ公式ドキュメント: https://pkg.go.dev/database/sql/driver - Go言語
syncパッケージ公式ドキュメント: https://pkg.go.dev/sync
参考にした情報源リンク
golang.org/issue/3857: https://github.com/golang/go/issues/3857golang.org/issue/4459: https://github.com/golang/go/issues/4459- Go CL 7803043: https://golang.org/cl/7803043
- Go言語の並行処理に関する一般的な情報源 (例: Go Concurrency Patterns): https://go.dev/blog/concurrency-patterns
- Go言語のインターフェースと埋め込みに関する情報源 (例: Effective Go): https://go.dev/doc/effective_go#interfaces
- Go言語の
sync.Mutexの使用方法に関する情報源 (例: Go by Example: Mutexes): https://gobyexample.com/mutexes