[インデックス 18113] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおけるコネクションプールの挙動を修正するものです。具体的には、コネクションプール内の最後のコネクションが正しく払い出されないというバグを修正し、それに対応するテストケースを追加しています。
コミット
commit 0d12e24ebb037202c3324c230e075f1e448f6f34
Author: Marko Tiikkaja <marko@joh.to>
Date: Thu Dec 26 11:27:18 2013 -0800
database/sql: Use all connections in pool
The last connection in the pool was not being handed out correctly.
R=golang-codereviews, gobot, bradfitz
CC=golang-codereviews
https://golang.org/cl/40410043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0d12e24ebb037202c3324c230e075f1e448f6f34
元コミット内容
database/sql: Use all connections in pool
The last connection in the pool was not being handed out correctly.
R=golang-codereviews, gobot, bradfitz
CC=golang-codereviews
https://golang.org/cl/40410043
変更の背景
このコミットの背景には、Go言語の database/sql
パッケージが提供するデータベースコネクションプールにおける、特定の条件下でのコネクション払い出しの不具合がありました。コミットメッセージに「The last connection in the pool was not being handed out correctly.(プール内の最後のコネクションが正しく払い出されていなかった)」とあるように、コネクションプールの最大オープンコネクション数 (maxOpen
) が設定されている場合、その上限に達した際に、プール内に利用可能なフリーコネクションがあるにも関わらず、新しいコネクションが作成されるか、あるいはデッドロックが発生する可能性がありました。
具体的には、db.maxOpen
が設定されており、かつ db.numOpen
(現在オープンしているコネクション数) が db.maxOpen
に達している状況で、db.freeConn
(利用可能なフリーコネクションのリスト) にコネクションが存在する場合、既存のロジックではこのフリーコネクションが利用されず、新しいコネクションを要求する処理が実行されてしまうという問題がありました。これにより、コネクションプールの効率が低下したり、最悪の場合、アプリケーションがデッドロックに陥る可能性がありました。このコミットは、この論理的な誤りを修正し、プール内のすべてのコネクションが適切に利用されるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、Go言語の database/sql
パッケージにおける以下の概念を理解しておく必要があります。
database/sql
パッケージ: Go言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。特定のデータベースドライバに依存しない抽象化レイヤーであり、アプリケーションはドライバを切り替えるだけで異なるデータベースに接続できます。- コネクションプール (Connection Pool): データベースへの接続はコストの高い操作です。コネクションプールは、事前に確立されたデータベースコネクションのセットを管理し、必要に応じてアプリケーションに提供し、使用後に再利用することで、コネクションの確立・切断のオーバーヘッドを削減し、アプリケーションのパフォーマンスを向上させます。
DB.SetMaxOpenConns(n int)
:database/sql
パッケージのDB
オブジェクトのメソッドで、同時にオープンできるデータベースコネクションの最大数を設定します。この設定は、データベースサーバーへの負荷を制御し、リソースの枯渇を防ぐために重要です。n
が0の場合は無制限を意味します。db.numOpen
:DB
オブジェクトが現在オープンしている(使用中またはフリーな)データベースコネクションの総数を追跡する内部カウンタです。db.freeConn
:DB
オブジェクトが管理する、現在使用可能でプールに戻された(アイドル状態の)コネクションのリストです。新しいコネクションが必要になった場合、まずこのリストから利用可能なコネクションが探されます。db.conn()
メソッド:database/sql
パッケージの内部メソッドで、新しいデータベースコネクションを取得するロジックをカプセル化しています。このメソッドが、コネクションプールから既存のコネクションを再利用するか、新しいコネクションを作成するかを決定します。
このコミットの修正は、特に db.conn()
メソッド内のコネクション払い出しロジック、特に db.maxOpen
の設定と db.freeConn
の状態を評価する条件式に焦点を当てています。
技術的詳細
このコミットの技術的な核心は、src/pkg/database/sql/sql.go
ファイル内の db.conn()
メソッドにおける条件式の変更にあります。
変更前のコードは以下のようになっていました。
// If db.maxOpen > 0 and the number of open connections is over the limit
// or there are no free connection, then make a request and wait.
if db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {
// ...
}
この条件式は、「db.maxOpen
が0より大きく、かつ (db.numOpen
が db.maxOpen
以上である または db.freeConn
が空である)」場合に、新しいコネクションを要求し、待機するというロジックでした。
この問題は、db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0)
の ||
(OR) 演算子にありました。
例えば、db.maxOpen
が1に設定されており、db.numOpen
も1であるとします。このとき、db.freeConn
にコネクションが1つ存在する場合を考えます。
変更前の条件式では、db.maxOpen > 0
は true
です。そして、db.numOpen >= db.maxOpen
(1 >= 1) も true
です。
この場合、true || db.freeConn.Len() == 0
は true
と評価されます。
結果として、true && true
となり、条件式全体が true
になります。
これは、db.numOpen
が db.maxOpen
に達しているという理由だけで、たとえ db.freeConn
に利用可能なコネクションがあったとしても、新しいコネクションを要求する(または待機する)ロジックが実行されてしまうことを意味します。本来であれば、フリーコネクションがあればそれを再利用すべきです。このバグにより、プール内の最後のコネクションが適切に利用されず、デッドロックや不必要なコネクション作成が発生する可能性がありました。
修正後のコードは以下のようになります。
// If db.maxOpen > 0 and the number of open connections is over the limit
// and there are no free connection, make a request and wait.
if db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {
// ...
}
変更点としては、db.numOpen >= db.maxOpen || db.freeConn.Len() == 0
が db.numOpen >= db.maxOpen && db.freeConn.Len() == 0
に変更されたことです。||
(OR) 演算子が &&
(AND) 演算子に置き換えられました。
この修正により、新しいコネクションを要求し待機する条件は、「db.maxOpen
が0より大きく、かつ db.numOpen
が db.maxOpen
以上であり、かつ db.freeConn
が空である」という、より厳密なものになりました。
上記の例で再評価すると、db.maxOpen
が1、db.numOpen
が1、db.freeConn
にコネクションが1つ存在する場合、
db.maxOpen > 0
は true
です。
db.numOpen >= db.maxOpen
(1 >= 1) は true
です。
db.freeConn.Len() == 0
は false
です(フリーコネクションが存在するため)。
したがって、true && true && false
となり、条件式全体が false
に評価されます。
これにより、フリーコネクションが存在する限り、新しいコネクションを要求するロジックは実行されず、既存のフリーコネクションが優先的に利用されるようになります。この変更によって、コネクションプールの動作がより意図通りになり、効率的なリソース利用とデッドロックの回避が実現されました。
また、この修正を検証するために src/pkg/database/sql/sql_test.go
に TestSingleOpenConn
という新しいテストケースが追加されました。このテストは、SetMaxOpenConns(1)
を設定した状態で、2回連続でクエリを実行し、デッドロックが発生しないことを確認するものです。これは、まさに修正されたバグが引き起こす可能性のあるシナリオを再現し、修正が正しく機能していることを保証するためのものです。
コアとなるコードの変更箇所
src/pkg/database/sql/sql.go
の db.conn()
メソッド内の条件式が変更されました。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -620,8 +620,8 @@ func (db *DB) conn() (*driverConn, error) {
}\n \n // If db.maxOpen > 0 and the number of open connections is over the limit
-\t// or there are no free connection, then make a request and wait.\n-\tif db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {\n+\t// and there are no free connection, make a request and wait.\n+\tif db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {\n \t\t// Make the connRequest channel. It\'s buffered so that the\n \t\t// connectionOpener doesn\'t block while waiting for the req to be read.\n \t\tch := make(chan interface{}, 1)
src/pkg/database/sql/sql_test.go
に新しいテストケース TestSingleOpenConn
が追加されました。
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1033,6 +1033,29 @@ func TestMaxOpenConns(t *testing.T) {\n \t}\n }\n \n+func TestSingleOpenConn(t *testing.T) {\n+\tdb := newTestDB(t, \"people\")\n+\tdefer closeDB(t, db)\n+\n+\tdb.SetMaxOpenConns(1)\n+\n+\trows, err := db.Query(\"SELECT|people|name|\")\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif err = rows.Close(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t// shouldn\'t deadlock\n+\trows, err = db.Query(\"SELECT|people|name|\")\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif err = rows.Close(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+}\n+\n // golang.org/issue/5323\n func TestStmtCloseDeps(t *testing.T) {\n \tif testing.Short() {
コアとなるコードの解説
src/pkg/database/sql/sql.go
の変更
変更された行は、db.conn()
メソッド内のコネクション取得ロジックの一部です。
元のコード:
if db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {
修正後のコード:
if db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {
この変更は、条件式の論理演算子を ||
(OR) から &&
(AND) に変更したものです。
- 変更前:
db.maxOpen
が設定されており、かつ「オープンコネクション数が上限に達している」または「フリーコネクションがない」場合に、新しいコネクションを要求する。この「または」が問題でした。フリーコネクションがあるにも関わらず、オープンコネクション数が上限に達しているという条件だけで新しいコネクションを要求してしまう可能性がありました。 - 変更後:
db.maxOpen
が設定されており、かつ「オープンコネクション数が上限に達している」かつ「フリーコネクションがない」場合にのみ、新しいコネクションを要求する。これにより、フリーコネクションが存在する場合は、必ずそちらが優先的に利用されるようになり、コネクションプールの意図された動作が保証されます。
src/pkg/database/sql/sql_test.go
の追加テスト
TestSingleOpenConn
は、この修正が正しく機能していることを検証するための重要なテストケースです。
func TestSingleOpenConn(t *testing.T) {
db := newTestDB(t, "people") // テスト用のDBインスタンスを作成
defer closeDB(t, db) // テスト終了時にDBをクローズ
db.SetMaxOpenConns(1) // 最大オープンコネクション数を1に設定
// 1回目のクエリ実行
rows, err := db.Query("SELECT|people|name|")
if err != nil {
t.Fatal(err)
}
if err = rows.Close(); err != nil {
t.Fatal(err)
}
// 2回目のクエリ実行 - デッドロックが発生しないことを確認
// "shouldn't deadlock" というコメントが意図を明確にしている
rows, err = db.Query("SELECT|people|name|")
if err != nil {
t.Fatal(err)
}
if err = rows.Close(); err != nil {
t.Fatal(err)
}
}
このテストの目的は、SetMaxOpenConns(1)
という非常に厳しいコネクション制限の下で、連続してクエリを実行してもデッドロックが発生しないことを確認することです。
db.SetMaxOpenConns(1)
: コネクションプールが同時に保持できるコネクション数を1に制限します。- 1回目の
db.Query()
: 新しいコネクションが作成され、使用されます。db.numOpen
は1になります。rows.Close()
が呼ばれると、このコネクションはプールに戻され、db.freeConn
に追加されます。 - 2回目の
db.Query()
: ここが重要です。修正前であれば、db.numOpen
がdb.maxOpen
(どちらも1) に達しているため、db.numOpen >= db.maxOpen
がtrue
となり、db.freeConn
に利用可能なコネクションがあるにも関わらず、新しいコネクションを要求しようとしてデッドロックが発生する可能性がありました。修正後は、db.numOpen >= db.maxOpen && db.freeConn.Len() == 0
という条件が評価され、db.freeConn.Len() == 0
がfalse
であるため、条件全体がfalse
となり、既存のフリーコネクションが再利用されます。
このテストは、修正されたロジックが、コネクションプールが最大オープンコネクション数に達している状況でも、フリーコネクションを正しく再利用し、デッドロックを回避できることを保証します。
関連リンク
- Go CL 40410043: https://golang.org/cl/40410043
参考にした情報源リンク
- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
database/sql
のコネクションプールに関する一般的な解説記事 (例: "Go database/sql connection pool explained" などで検索)- (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
- Go言語の論理演算子に関するドキュメントやチュートリアル (例: "Go logical operators")
- (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
- Go言語のテストに関するドキュメントやチュートリアル (例: "Go testing")
- (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
- Go言語のデッドロックに関する一般的な解説 (例: "Go deadlock")
- (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
- Go言語の
database/sql
パッケージのソースコード (特にsql.go
とsql_test.go
)- https://github.com/golang/go/blob/master/src/database/sql/sql.go
- https://github.com/golang/go/blob/master/src/database/sql/sql_test.go
- (コミット当時のバージョンとは異なる可能性がありますが、現在のコードも参考になります) 解説の生成が完了しました。