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

[インデックス 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言語の mapnil

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 が追加されています。このテストケースは、以下の手順でクラッシュを再現しようとします。

  1. TransportClient を作成します。
  2. テスト用のHTTPサーバーを httptest.NewServer で起動します。このサーバーは、リクエストを受け取ると unblockCh からの信号を待ち、その後 tr.CloseIdleConnections() を呼び出します。
  3. ゴルーチン内で c.Get(ts.URL) を呼び出し、HTTPリクエストを送信します。このリクエストは、サーバー側で CloseIdleConnections が呼び出されるまでブロックされます。
  4. メインゴルーチンで unblockCh <- true を送信し、サーバー側の処理を続行させます。
  5. サーバー側で CloseIdleConnections が呼び出された後、クライアント側でレスポンスボディを閉じます。この res.Body.Close() の呼び出しが、コネクションをアイドル状態に戻し、idleConn マップにアクセスしようとします。

このシーケンスにより、CloseIdleConnectionsidleConnnil に設定した直後に、別のゴルーチンがその nil マップにアクセスしようとする状況を作り出し、バグが再現されることを確認しています。修正後は、このテストがパニックを起こさずに正常に完了するようになります。

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

変更は以下の2つのファイルで行われています。

  1. src/pkg/net/http/transport.go
  2. 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 という新しいテスト関数が追加されています。

  1. tr := &Transport{}c := &Client{Transport: tr}: テスト対象となる Transport インスタンスと、それを使用する Client インスタンスを作成します。

  2. unblockCh := make(chan bool, 1): サーバー側のハンドラがブロックされ、特定のタイミングで CloseIdleConnections を呼び出すためのチャネルです。バッファリングされたチャネル(容量1)を使用することで、送信側が受信側を待たずに値を送信できます。

  3. ts := httptest.NewServer(...): テスト用のHTTPサーバーを起動します。このサーバーのハンドラは以下のロジックを持ちます。

    • <-unblockCh: unblockCh から値が送信されるまでブロックします。これにより、クライアントからのリクエストがサーバー側で一時停止します。
    • tr.CloseIdleConnections(): unblockCh からの信号を受け取った後、テスト対象の Transport インスタンスの CloseIdleConnections メソッドを呼び出します。これがバグを誘発する操作です。
  4. go func() { ... }(): 別のゴルーチンでクライアントからのHTTPリクエストを送信します。

    • res, err := c.Get(ts.URL): テストサーバーへのGETリクエストを送信します。この呼び出しは、サーバー側が unblockCh からの信号を受け取り、レスポンスを返すまでブロックされます。
    • res.Body.Close(): レスポンスボディを閉じます。net/httpTransport は、レスポンスボディが閉じられた際に、コネクションをアイドルプールに戻そうとします。この操作が、CloseIdleConnections によって nil に設定された idleConn マップへのアクセスを試み、クラッシュを引き起こす可能性がありました。
    • didreq <- true: リクエストが完了したことをメインゴルーチンに通知します。
  5. unblockCh <- true: メインゴルーチンから unblockCh に値を送信し、テストサーバーのハンドラをアンブロックします。これにより、サーバー側で CloseIdleConnections が呼び出されます。

  6. <-didreq: メインゴルーチンは didreq から値が送信されるまで待ちます。これにより、クライアントのリクエストが完全に処理され、コネクションがアイドルプールに戻される試みが完了するまでテストが終了しないことを保証します。

このテストは、CloseIdleConnections が呼び出された直後に、別のゴルーチンがアイドルコネクションをプールに戻そうとするという、競合状態(race condition)に近いシナリオをシミュレートしています。このシナリオでパニックが発生しないことを確認することで、修正が正しく機能していることを検証しています。

関連リンク

参考にした情報源リンク