[インデックス 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