[インデックス 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.gosrc/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)