[インデックス 12549] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の Transport.CloseIdleConnections
メソッドにおけるクラッシュバグを修正するものです。具体的には、アイドル状態のコネクションを閉じる際に発生する可能性があったnilポインタ参照などの問題を解決し、堅牢性を向上させています。
コミット
commit b2e9f425b92cd6b986051a55c24dc96b777d9f28
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Mar 9 16:27:32 2012 -0800
net/http: fix crash with Transport.CloseIdleConnections
Thanks Michael Lore for the bug report!
Fixes #3266
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5754068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b2e9f425b92cd6b986051a55c24dc96b777d9f28
元コミット内容
net/http: Transport.CloseIdleConnections
でのクラッシュを修正。
Michael Lore氏のバグレポートに感謝します!
Issue #3266 を修正。
変更の背景
このコミットは、Go言語の net/http
パッケージにおける Transport
型の CloseIdleConnections
メソッドが特定の条件下でクラッシュするバグ(Issue #3266)を修正するために行われました。
net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。Transport
はHTTPリクエストの単一のトランザクション(例えば、TCPコネクションの確立、リクエストの送信、レスポンスの受信)を処理するインターフェースです。通常、http.Client
は内部的に Transport
を使用してHTTP通信を行います。
Transport
は、パフォーマンス向上のためにHTTPコネクションを再利用する「コネクションプーリング」のメカニズムを持っています。これにより、同じホストへの複数のリクエストに対して、TCPコネクションの確立やTLSハンドシェイクのオーバーヘッドを削減できます。idleConn
は、再利用可能なアイドル状態のコネクションを保持するための内部マップです。
CloseIdleConnections
メソッドは、これらのアイドル状態のコネクションを強制的に閉じるために提供されています。これは、リソースの解放や、サーバー側のコネクションタイムアウトへの対応、あるいはテスト環境でのクリーンアップなどに利用されます。
報告されたバグ(Issue #3266)は、CloseIdleConnections
が呼び出された際に、t.idleConn
マップが nil
に設定されることによって発生していました。この状態の Transport
インスタンスに対して、後続の操作(特に新しいコネクションがアイドル状態になり、idleConn
に追加されようとする場合など)が行われると、nil
マップへのアクセスが発生し、パニック(クラッシュ)を引き起こす可能性がありました。Michael Lore氏によってこの問題が報告され、その修正がこのコミットで行われました。
前提知識の解説
Go言語の net/http
パッケージ
Go言語の net/http
パッケージは、HTTPクライアントとサーバーを構築するための強力な機能を提供します。
http.Client
: HTTPリクエストを送信するためのクライアントです。通常、Get
,Post
,Do
などのメソッドを通じてHTTP通信を行います。http.Transport
:http.Client
の内部で実際にHTTPリクエストを送信するメカニズムを定義します。TCPコネクションの確立、TLSハンドシェイク、プロキシ設定、コネクションプーリングなどを担当します。Transport
は複数のHTTPリクエスト間でコネクションを再利用することで、パフォーマンスを向上させます。- コネクションプーリング: HTTP/1.1では、同じサーバーへの複数のリクエストに対してTCPコネクションを再利用する「持続的接続(Persistent Connections)」がサポートされています。
http.Transport
はこの機能を利用し、使用済みでアイドル状態になったコネクションを内部のプール(idleConn
マップ)に保持します。これにより、新しいリクエストが来た際に、既存のコネクションを再利用でき、ネットワークオーバーヘッドを削減できます。 CloseIdleConnections()
メソッド:Transport
型のメソッドで、現在プールされているすべてのアイドル状態のコネクションを強制的に閉じます。これは、リソースを解放したり、サーバー側のコネクションタイムアウトに起因する問題を回避したりするために使用されます。
Go言語の map
と nil
Go言語において map
はキーと値のペアを格納するハッシュテーブルです。map
型の変数は、初期化されていない場合、そのゼロ値は nil
です。nil
マップに対して要素の追加や削除を行おうとすると、ランタイムパニック(クラッシュ)が発生します。
make(map[keyType]valueType)
:map
を初期化し、使用可能な状態にするための組み込み関数です。これにより、nil
ではないマップが作成され、要素の追加や削除が可能になります。
Go言語のテスト
Go言語には、標準でテストフレームワークが組み込まれています。
go test
: テストを実行するためのコマンドです。testing
パッケージ: テストコードを書くための基本的な機能を提供します。func TestXxx(t *testing.T)
: テスト関数はTest
で始まり、*testing.T
型の引数を取ります。t.Error()
/t.Fatal()
: テスト失敗を報告するためのメソッドです。t.Fatal()
はテストを即座に終了させます。httptest
パッケージ: HTTPサーバーのテストを容易にするためのユーティリティを提供します。httptest.NewServer
を使用すると、テスト用のHTTPサーバーを簡単に起動できます。
技術的詳細
このバグは、Transport.CloseIdleConnections()
メソッドが呼び出された際に、t.idleConn
マップが nil
に設定されることによって引き起こされていました。
元のコードでは、CloseIdleConnections
メソッドの最後に t.idleConn = nil
という行がありました。この行は、アイドルコネクションのマップをクリアする意図で書かれたものと思われますが、実際にはマップを nil
に設定してしまいます。
マップが nil
に設定された後、もし同じ Transport
インスタンスが再度使用され、新しいコネクションがアイドル状態になり、そのコネクションを idleConn
マップに追加しようとすると、nil
マップへの書き込み操作が発生し、Goランタイムがパニックを起こしていました。
修正は非常にシンプルで、t.idleConn = nil
の代わりに t.idleConn = make(map[string][]*persistConn)
を使用することで、マップを nil
にするのではなく、空の新しいマップで再初期化するように変更されています。これにより、CloseIdleConnections
が呼び出された後も t.idleConn
は有効なマップオブジェクトであり続け、後続の操作でパニックが発生するのを防ぎます。
また、この修正には、バグを再現し、修正が正しく機能することを確認するための新しいテストケース TestTransportIdleConnCrash
が追加されています。このテストケースは、以下の手順でクラッシュを再現しようとします。
Transport
とClient
を作成します。- テスト用のHTTPサーバーを
httptest.NewServer
で起動します。このサーバーは、リクエストを受け取るとunblockCh
からの信号を待ち、その後tr.CloseIdleConnections()
を呼び出します。 - ゴルーチン内で
c.Get(ts.URL)
を呼び出し、HTTPリクエストを送信します。このリクエストは、サーバー側でCloseIdleConnections
が呼び出されるまでブロックされます。 - メインゴルーチンで
unblockCh <- true
を送信し、サーバー側の処理を続行させます。 - サーバー側で
CloseIdleConnections
が呼び出された後、クライアント側でレスポンスボディを閉じます。このres.Body.Close()
の呼び出しが、コネクションをアイドル状態に戻し、idleConn
マップにアクセスしようとします。
このシーケンスにより、CloseIdleConnections
が idleConn
を nil
に設定した直後に、別のゴルーチンがその nil
マップにアクセスしようとする状況を作り出し、バグが再現されることを確認しています。修正後は、このテストがパニックを起こさずに正常に完了するようになります。
コアとなるコードの変更箇所
変更は以下の2つのファイルで行われています。
src/pkg/net/http/transport.go
src/pkg/net/http/transport_test.go
src/pkg/net/http/transport.go
の変更
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -196,7 +196,7 @@ func (t *Transport) CloseIdleConnections() {
pconn.close()
}
}
- t.idleConn = nil
+ t.idleConn = make(map[string][]*persistConn)
}
//
src/pkg/net/http/transport_test.go
の変更
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -698,6 +698,32 @@ func TestTransportPersistConnLeak(t *testing.T) {
}
}
+// This used to crash; http://golang.org/issue/3266
+func TestTransportIdleConnCrash(t *testing.T) {
+ tr := &Transport{}
+ c := &Client{Transport: tr}
+
+ unblockCh := make(chan bool, 1)
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ <-unblockCh
+ tr.CloseIdleConnections()
+ }))
+ defer ts.Close()
+
+ didreq := make(chan bool)
+ go func() {
+ res, err := c.Get(ts.URL)
+ if err != nil {
+ t.Error(err)
+ } else {
+ res.Body.Close() // returns idle conn
+ }
+ didreq <- true
+ }()
+ unblockCh <- true
+ <-didreq
+}
+
type fooProto struct{}
func (fooProto) RoundTrip(req *Request) (*Response, error) {
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更点
Transport.CloseIdleConnections()
メソッド内で、t.idleConn = nil
という行が t.idleConn = make(map[string][]*persistConn)
に変更されています。
-
変更前 (
t.idleConn = nil
): この行は、t.idleConn
マップをnil
に設定します。Go言語では、nil
マップは要素の追加や削除ができません。nil
マップに対して書き込み操作を行おうとすると、ランタイムパニックが発生します。このため、CloseIdleConnections
が呼び出された後、もし新しいアイドルコネクションがこのTransport
に戻されようとすると、クラッシュの原因となっていました。 -
変更後 (
t.idleConn = make(map[string][]*persistConn)
): この行は、t.idleConn
を新しい空のマップで再初期化します。make
関数によって作成されたマップはnil
ではないため、要素の追加や削除が安全に行えます。これにより、CloseIdleConnections
が呼び出された後もTransport
は正常に機能し続け、アイドルコネクションの管理を継続できます。これは、マップをクリアする正しい方法であり、後続の操作でのパニックを防ぎます。
src/pkg/net/http/transport_test.go
の追加テスト
TestTransportIdleConnCrash
という新しいテスト関数が追加されています。
-
tr := &Transport{}
とc := &Client{Transport: tr}
: テスト対象となるTransport
インスタンスと、それを使用するClient
インスタンスを作成します。 -
unblockCh := make(chan bool, 1)
: サーバー側のハンドラがブロックされ、特定のタイミングでCloseIdleConnections
を呼び出すためのチャネルです。バッファリングされたチャネル(容量1)を使用することで、送信側が受信側を待たずに値を送信できます。 -
ts := httptest.NewServer(...)
: テスト用のHTTPサーバーを起動します。このサーバーのハンドラは以下のロジックを持ちます。<-unblockCh
:unblockCh
から値が送信されるまでブロックします。これにより、クライアントからのリクエストがサーバー側で一時停止します。tr.CloseIdleConnections()
:unblockCh
からの信号を受け取った後、テスト対象のTransport
インスタンスのCloseIdleConnections
メソッドを呼び出します。これがバグを誘発する操作です。
-
go func() { ... }()
: 別のゴルーチンでクライアントからのHTTPリクエストを送信します。res, err := c.Get(ts.URL)
: テストサーバーへのGETリクエストを送信します。この呼び出しは、サーバー側がunblockCh
からの信号を受け取り、レスポンスを返すまでブロックされます。res.Body.Close()
: レスポンスボディを閉じます。net/http
のTransport
は、レスポンスボディが閉じられた際に、コネクションをアイドルプールに戻そうとします。この操作が、CloseIdleConnections
によってnil
に設定されたidleConn
マップへのアクセスを試み、クラッシュを引き起こす可能性がありました。didreq <- true
: リクエストが完了したことをメインゴルーチンに通知します。
-
unblockCh <- true
: メインゴルーチンからunblockCh
に値を送信し、テストサーバーのハンドラをアンブロックします。これにより、サーバー側でCloseIdleConnections
が呼び出されます。 -
<-didreq
: メインゴルーチンはdidreq
から値が送信されるまで待ちます。これにより、クライアントのリクエストが完全に処理され、コネクションがアイドルプールに戻される試みが完了するまでテストが終了しないことを保証します。
このテストは、CloseIdleConnections
が呼び出された直後に、別のゴルーチンがアイドルコネクションをプールに戻そうとするという、競合状態(race condition)に近いシナリオをシミュレートしています。このシナリオでパニックが発生しないことを確認することで、修正が正しく機能していることを検証しています。
関連リンク
- Go Issue #3266: http://golang.org/issue/3266
- Go CL 5754068: https://golang.org/cl/5754068
参考にした情報源リンク
- Go言語の公式ドキュメント:
net/http
パッケージ - Go言語の公式ドキュメント:
testing
パッケージ - Go言語の公式ドキュメント:
httptest
パッケージ - Go言語のマップに関するドキュメント
- HTTP/1.1 Persistent Connections (RFC 2616 Section 8.1.2.1)
- Go言語のチャネルに関するドキュメント
- https://go.dev/tour/concurrency/2
- https://go.dev/tour/concurrency/3 (Buffered Channels)
- Go言語のゴルーチンに関するドキュメント
- Go言語の競合状態(Race Condition)に関する一般的な情報
- https://go.dev/doc/articles/race_detector
- https://go.dev/blog/go-concurrency-patterns-pipelines (Concurrency Patterns)