Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15647] ファイルの概要

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける接続リーク(Conn leak)の修正を目的としています。具体的には、DB.Prepare メソッドが Stmt オブジェクトを準備する際に、内部的に使用したデータベース接続(Conn)が適切に接続プールに返却されない問題を解決します。これにより、アプリケーションが Prepare を繰り返し呼び出すと、利用可能なデータベース接続が枯渇し、最終的にアプリケーションのパフォーマンス低下や停止を引き起こす可能性がありました。

コミット

commit 3cdf8bae1a47948804d782ef5e8c4de38ea0ac9b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Mar 8 10:04:17 2013 -0800

    database/sql: fix Conn leak
    
    Fixes #4902
    
    R=golang-dev, alex.brainman, r, google
    CC=golang-dev
    https://golang.org/cl/7579045
---
 src/pkg/database/sql/sql.go      |  1 +
 src/pkg/database/sql/sql_test.go | 29 +++++++++++++++++++++++++++++
 2 files changed, 30 insertions(+)

diff --git a/src/pkg/database/sql/sql.go b/src/pkg/database/sql/sql.go
index 4faaa11b11..6d52d2986d 100644
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -445,6 +445,7 @@ func (db *DB) prepare(query string) (*Stmt, error) {
 	\t\tcss:   []connStmt{{ci, si}},\n \t}\n \tdb.addDep(stmt, stmt)\n+\tdb.putConn(ci, nil)\n \treturn stmt, nil\n }\n \ndiff --git a/src/pkg/database/sql/sql_test.go b/src/pkg/database/sql/sql_test.go
index 53b229600d..f5c3f1ed65 100644
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -708,3 +708,32 @@ func TestQueryRowNilScanDest(t *testing.T) {
 \t\tt.Errorf(\"error = %q; want %q\", err.Error(), want)\n \t}\n }\n+\n+func TestIssue4902(t *testing.T) {\n+\tdb := newTestDB(t, \"people\")\n+\tdefer closeDB(t, db)\n+\n+\tdriver := db.driver.(*fakeDriver)\n+\topens0 := driver.openCount\n+\n+\tvar stmt *Stmt\n+\tvar err error\n+\tfor i := 0; i < 10; i++ {\n+\t\tstmt, err = db.Prepare(\"SELECT|people|name|\")\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\terr = stmt.Close()\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t}\n+\n+\topens := driver.openCount - opens0\n+\tif opens > 1 {\n+\t\tt.Errorf(\"opens = %d; want <= 1\", opens)\n+\t\tt.Logf(\"db = %#v\", db)\n+\t\tt.Logf(\"driver = %#v\", driver)\n+\t\tt.Logf(\"stmt = %#v\", stmt)\n+\t}\n+}\n```

## GitHub上でのコミットページへのリンク

[https://github.com/golang/go/commit/3cdf8bae1a47948804d782ef5e8c4de38ea0ac9b](https://github.com/golang/go/commit/3cdf8bae1a47948804d782ef5e8c4de38ea0ac9b)

## 元コミット内容

database/sql: fix Conn leak

Fixes #4902

R=golang-dev, alex.brainman, r, google CC=golang-dev https://golang.org/cl/7579045


## 変更の背景

このコミットは、Go言語の `database/sql` パッケージにおける重要なバグ、具体的には「接続リーク(Conn leak)」を修正するために導入されました。コミットメッセージに `Fixes #4902` とあるように、これはGoのIssueトラッカーで報告された問題(Issue 4902)に対応するものです。

`database/sql` パッケージは、Goアプリケーションからリレーショナルデータベースにアクセスするための汎用的なインターフェースを提供します。このパッケージは、データベース接続のプールを管理し、アプリケーションがデータベース操作を行う際に効率的に接続を再利用できるように設計されています。しかし、接続が適切に解放されず、プールに返却されない場合、接続リークが発生します。接続リークが発生すると、利用可能なデータベース接続の数が徐々に減少し、最終的には新しい接続を確立できなくなり、アプリケーションがデータベース操作を実行できなくなるという深刻な問題を引き起こします。

この特定のケースでは、`DB.Prepare` メソッドがプリペアドステートメント(`Stmt`)を作成する際に、内部的に取得したデータベース接続が、`Stmt` オブジェクトのライフサイクルとは独立して適切にプールに返却されないという問題がありました。`Prepare` メソッドは、データベースに対してクエリを準備し、その結果として `Stmt` オブジェクトを返します。この準備プロセス中に、データベースとの通信のために一時的に接続が使用されます。もしこの接続が `Prepare` の完了後にプールに返却されないと、`Prepare` が繰り返し呼び出されるたびに、新しい接続がプールから消費され続け、リークが発生します。

このリークは、特に高負荷なアプリケーションや、`Prepare` を頻繁に呼び出すようなパターンを持つアプリケーションにおいて、深刻な影響を及ぼす可能性がありました。利用可能な接続が枯渇すると、アプリケーションはデータベースへのアクセスを待機するか、エラーを返すようになり、結果としてサービス停止やパフォーマンスの大幅な低下につながります。このコミットは、この根本的な問題を解決し、`database/sql` パッケージの堅牢性と信頼性を向上させることを目的としています。

## 前提知識の解説

このコミットの変更内容を理解するためには、Go言語の `database/sql` パッケージの基本的な概念と、データベース接続管理に関する知識が必要です。

1.  **`database/sql` パッケージ**:
    *   Goの標準ライブラリの一部であり、SQLデータベースとの対話のための汎用的なインターフェースを提供します。特定のデータベースドライバに依存しない抽象化レイヤーとして機能し、アプリケーションコードはデータベースの種類を意識せずに記述できます。
    *   主な型として `DB`、`Conn`、`Stmt`、`Tx`、`Rows` などがあります。

2.  **`DB` オブジェクト**:
    *   `sql.Open` 関数によって作成され、データベースへの抽象的な接続を表します。
    *   内部的には、データベース接続のプールを管理しています。アプリケーションがデータベース操作を行う際に、このプールから接続を取得し、操作完了後に接続をプールに返却します。これにより、接続の確立と切断のオーバーヘッドを削減し、効率的なデータベースアクセスを実現します。

3.  **`Conn` オブジェクト**:
    *   `DB` オブジェクトが管理する個々の物理的なデータベース接続を表します。
    *   通常、アプリケーション開発者が直接 `Conn` を操作することは稀で、`DB` オブジェクトを介して間接的に利用されます。

4.  **`Stmt` オブジェクト(プリペアドステートメント)**:
    *   `DB.Prepare` または `Tx.Prepare` メソッドによって作成されます。
    *   データベースに対して事前にコンパイルされたSQLステートメントを表します。これにより、同じSQLクエリを異なるパラメータで複数回実行する際に、クエリの解析と最適化のオーバーヘッドを削減できます。
    *   `Stmt` オブジェクトは、作成時にデータベース接続を内部的に保持することがあります。この接続は、ステートメントの実行(`Exec` や `Query`)に使用されます。

5.  **接続プール(Connection Pool)**:
    *   データベース接続の再利用を目的としたメカニズムです。新しい接続を確立するコストは高いため、アプリケーションは一度確立した接続をプールに保持し、必要に応じて再利用します。
    *   接続リークは、このプールに接続が返却されない場合に発生し、利用可能な接続が枯渇する原因となります。

6.  **リソース管理の重要性**:
    *   データベース接続のようなリソースは有限であり、適切に管理されないと、アプリケーションのパフォーマンスや安定性に悪影響を及ぼします。
    *   `database/sql` パッケージでは、`Rows.Close()`、`Tx.Commit()`/`Tx.Rollback()` など、リソースを明示的に解放するためのメソッドが提供されています。これらのメソッドを `defer` ステートメントと組み合わせて使用することが、リークを防ぐための一般的なプラクティスです。

このコミットは、`DB.Prepare` メソッドが `Stmt` オブジェクトを作成する過程で、内部的に使用した `Conn` オブジェクトを接続プールに適切に返却していなかったという、このリソース管理の不備を修正するものです。

## 技術的詳細

このコミットの技術的な核心は、`database/sql` パッケージの `DB` 型に属する内部メソッド `prepare` の変更にあります。

`DB.Prepare` メソッドは、ユーザーがSQLクエリ文字列を渡してプリペアドステートメントを作成する際に呼び出されます。このメソッドの内部では、実際にデータベースドライバに対してプリペアドステートメントの準備を要求するために、データベース接続(`Conn`)が必要となります。

変更前のコードでは、`prepare` メソッドは以下のような処理を行っていました(簡略化):

1.  `db.conn()` を呼び出して、接続プールから利用可能な `Conn` オブジェクト(`ci`)を取得します。
2.  取得した `ci` を使用して、データベースドライバの `Prepare` メソッドを呼び出し、プリペアドステートメント(`si`)を作成します。
3.  `Stmt` オブジェクトを構築し、`ci` と `si` を関連付けて、`db.addDep` を介して `DB` オブジェクトに依存関係として追加します。
4.  `Stmt` オブジェクトを呼び出し元に返します。

問題は、ステップ3の後に `ci` (取得した `Conn` オブジェクト) が接続プールに明示的に返却されていなかった点にあります。`Stmt` オブジェクトが `ci` を内部的に参照しているため、`ci` は `Stmt` オブジェクトがクローズされるまで「使用中」の状態にあると見なされるべきですが、`prepare` メソッドのコンテキストでは、`Stmt` オブジェクトの作成が完了した時点で、`prepare` メソッド自身が `ci` を保持し続ける必要はありませんでした。

このコミットでは、`prepare` メソッドの最後に `db.putConn(ci, nil)` という行が追加されました。

*   `db.putConn(ci, nil)`: このメソッドは、指定された `Conn` オブジェクト `ci` を接続プールに返却する役割を担います。第二引数の `nil` は、この接続が特定の `Stmt` や `Tx` に関連付けられていないことを示唆しています(この場合は、`Stmt` が作成された後、`prepare` メソッドのコンテキストでは `ci` はもはや直接的な依存関係を持たないため)。

この変更により、`prepare` メソッドが `Conn` オブジェクトを一時的に使用した後、その `Conn` を直ちに接続プールに返却するようになりました。これにより、`Stmt` オブジェクトが作成された後も `Conn` がリークし続けることがなくなり、接続プールが適切に管理されるようになります。

テストコード `TestIssue4902` は、この修正が正しく機能することを確認するために追加されました。このテストは、`DB.Prepare` と `Stmt.Close` を繰り返し呼び出し、その間に開かれた接続の数が期待通りに増加しない(つまり、リークが発生しない)ことを検証します。`driver.openCount` を監視することで、物理的なデータベース接続が不必要に開かれ続けないことを確認しています。

## コアとなるコードの変更箇所

`src/pkg/database/sql/sql.go` ファイルの `func (db *DB) prepare(query string) (*Stmt, error)` メソッドに以下の1行が追加されました。

```diff
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -445,6 +445,7 @@ func (db *DB) prepare(query string) (*Stmt, error) {
 	\t\tcss:   []connStmt{{ci, si}},\n \t}\n \tdb.addDep(stmt, stmt)\n+\tdb.putConn(ci, nil)\n \treturn stmt, nil
 }\n 

また、src/pkg/database/sql/sql_test.goTestIssue4902 という新しいテスト関数が追加されました。

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -708,3 +708,32 @@ func TestQueryRowNilScanDest(t *testing.T) {
 \t\tt.Errorf(\"error = %q; want %q\", err.Error(), want)\n \t}\n }\n+\n+func TestIssue4902(t *testing.T) {\n+\tdb := newTestDB(t, \"people\")\n+\tdefer closeDB(t, db)\n+\n+\tdriver := db.driver.(*fakeDriver)\n+\topens0 := driver.openCount\n+\n+\tvar stmt *Stmt\n+\tvar err error\n+\tfor i := 0; i < 10; i++ {\n+\t\tstmt, err = db.Prepare(\"SELECT|people|name|\")\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\terr = stmt.Close()\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t}\n+\n+\topens := driver.openCount - opens0\n+\tif opens > 1 {\n+\t\tt.Errorf(\"opens = %d; want <= 1\", opens)\n+\t\tt.Logf(\"db = %#v\", db)\n+\t\tt.Logf(\"driver = %#v\", driver)\n+\t\tt.Logf(\"stmt = %#v\", stmt)\n+\t}\n+}\n```

## コアとなるコードの解説

追加された `db.putConn(ci, nil)` の行は、`database/sql` パッケージの接続管理において非常に重要な役割を果たします。

`db.prepare` メソッドは、プリペアドステートメントを作成するために、まず `db.conn()` を呼び出してデータベース接続 `ci` を取得します。この `ci` は、データベースドライバの `Prepare` メソッドを呼び出すために一時的に使用されます。`Prepare` メソッドが完了し、プリペアドステートメント `si` が作成されると、`Stmt` オブジェクトが構築され、この `Stmt` オブジェクトが `ci` と `si` を内部的に保持します。

変更前は、`Stmt` オブジェクトが作成された後、`prepare` メソッドのスコープ内で `ci` が明示的に接続プールに返却されていませんでした。これは、`Stmt` オブジェクトが `ci` を参照しているため、`ci` は `Stmt` がクローズされるまで使用中であると見なされるべきですが、`prepare` メソッド自身が `ci` を保持し続ける必要はないという微妙な状況でした。結果として、`prepare` が呼び出されるたびに新しい接続がプールから取得され、それが適切に返却されないままになり、接続リークが発生していました。

`db.putConn(ci, nil)` の追加により、`prepare` メソッドは `Stmt` オブジェクトの作成が完了した直後に、一時的に使用した `ci` を接続プールに返却するようになりました。
*   `ci`: 接続プールに返却される `Conn` オブジェクト。
*   `nil`: この引数は、返却される接続が特定の `Stmt` や `Tx` に関連付けられていないことを示します。この場合、`Stmt` オブジェクトは `ci` を内部的に参照し続けるものの、`prepare` メソッドのコンテキストからは `ci` が解放され、プールに再利用可能としてマークされます。

これにより、`Stmt` オブジェクトがクローズされるまで `ci` が「使用中」の状態を維持しつつも、`prepare` メソッドが完了した時点で `ci` が接続プールに返却され、他の操作のために再利用できるようになります。この修正は、`database/sql` パッケージの接続プールの効率的な運用を保証し、接続リークを防ぐ上で不可欠です。

追加された `TestIssue4902` は、この修正の有効性を検証するためのものです。このテストでは、`DB.Prepare` と `Stmt.Close` を複数回実行し、その間に `fakeDriver` の `openCount`(開かれた接続の数)が不必要に増加しないことを確認します。もしリークがあれば `openCount` が増え続けるため、このテストはリークの発生を検出できます。テストが `opens > 1` でエラーを報告するというアサーションは、`Prepare` が繰り返し呼び出されても、開かれる接続の数が1つ以下に保たれるべきであることを示しています。これは、接続が適切にプールに返却され、再利用されていることを意味します。

## 関連リンク

*   GitHubコミットページ: [https://github.com/golang/go/commit/3cdf8bae1a47948804d782ef5e8c4de38ea0ac9b](https://github.com/golang/go/commit/3cdf8bae1a47948804d782ef5e8c4de38ea0ac9b)
*   Go Code Review (CL): [https://golang.org/cl/7579045](https://golang.org/cl/7579045)
*   Go Issue 4902 (直接的な公開情報は見つかりませんでしたが、コミットメッセージで参照されています)

## 参考にした情報源リンク

*   Go `database/sql` パッケージのドキュメント: [https://pkg.go.dev/database/sql](https://pkg.go.dev/database/sql)
*   Go `database/sql` 接続リークに関する一般的な情報:
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHLmWMQQQ4Z9SIm1EqaVZ8T3te0H02batwPqOU_YbbvFrZF1sIQVMUu7mmlL6JrROht-T5rEdTX5I0Fv-1gGIlVXM8qT0TOOvpiNc4aKcAtIsJSGg79NugoSafJYRyeDiRHQkB0rtvcOsr_hod6VX_VyufJMOwwtZGGTS6iqQroajtoGYWeXwOQhBMeISWgYgBz](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHLmWMQQQ4Z9SIm1EqaVZ8T3te0H02batwPqOU_YbbvFrZF1sIQVMUu7mmlL6JrROht-T5rEdTX5I0Fv-1gGIlVXM8qT0TOOvpiNc4aKcAtIsJSGg79NugoSafJYRyeDiRHQkB0rtvcOsr_hod6VX_VyufJMOwwtZGGTS6iqQroajtoGYWeXwOQhBMeISWgYgBz)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHUuZHCR1PRAckfwUss-C0izOkEOif8Xyq5mmx5nWeHT-4np7uA557Sqw6eO_sr-anpF6lfCk-1A6BzpCUKBdCZL-m_o-u5bp4cQBD3DTv3M-_CrjfxCooXAXXt1EzZAZOPxBlYq1FWlhsyZS7aVxrlZBbWNFRhlGp-z20i6FcoSBeIywzfX8a6](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHUuZHCR1PRAckfwUss-C0izOkEOif8Xyq5mmx5nWeHT-4np7uA557Sqw6eO_sr-anpF6lfCk-1A6BzpCUKBdCZL-m_o-u5bp4cQBD3DTv3M-_CrjfxCooXAXXt1EzZAZOPxBlYq1FWlhsyZS7aVxrlZBbWNFRhlGp-z20i6FcoSBeIywzfX8a6)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFNgn7uKafEPI-C5qcYsHPKO6S7KvMDq6TOFxMdgmsaOT5z0L_z3iIzUcJHLTJzIn7mFp15H0aMthm8OMNhx2Gewe_Ra1_IdEG4TxGX6NYesWwD8A0_vIcE-Xt5B4OGTFTGTWqxJHzPtIvkkkVMlJBwq1at3NP-jE1FMZWr733WyUEXd](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFNgn7uKafEPI-C5qcYsHPKO6S7KvMDq6TOFxMdgmsaOT5z0L_z3iIzUcJHLTJzIn7mFp15H0aMthm8OMNhx2Gewe_Ra1_IdEG4TxGX6NYesWwD8A0_vIcE-Xt5B4OGTFTGTWqxJHzPtIvkkkVMlJBwq1at3NP-jE1FMZWr733WyUEXd)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGmYrsMzFQnk_1FH9PqcXNB46CIt0cbo2OSpdLqfF8kX7JqIcCCe7sm1WQP8fA9u-L5HvOEZZ--OEz5GnrxSuE2J3vUcZim4IEmKi8hG-bldYNR4LCR4ozVMwXNbbGsEwYGyYY](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGmYrsMzFQnk_1FH9PqcXNB46CIt0cbo2OSpdLqfF8kX7JqIcCCe7sm1WQP8fA9u-L5HvOEZZ--OEz5GnrxSuE2J3vUcZim4IEmKi8hG-bldYNR4LCR4ozVMwXNbbGsEwYGyYY)