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

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

このコミットは、net/http パッケージの serve_test.go ファイルに新しいテストケースを追加するものです。このテストは、Request.Body.Read が別のゴルーチンでハングしている場合に、ハンドラゴルーチンの Request.Body.Close がブロックされないことを検証することを目的としています。ただし、このテストは既知の問題(golang.org/issue/7121)のため、現時点ではスキップされています。

コミット

commit 35710eecd64097598ba33166692fba54078d6b34
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 15 13:12:32 2014 -0800

    net/http: add disabled test for Body Read/Close lock granularity
    
    Update #7121
    
    R=golang-codereviews, gobot, dsymonds
    CC=golang-codereviews
    https://golang.org/cl/51750044

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

https://github.com/golang/go/commit/35710eecd64097598ba33166692fba54078d6b34

元コミット内容

net/http: add disabled test for Body Read/Close lock granularity

このコミットは、net/http パッケージにおいて、Request.BodyRead および Close メソッドのロックの粒度に関する無効化されたテストを追加します。これは、Issue #7121 に関連するものです。

変更の背景

この変更の背景には、Goの net/http パッケージにおけるリクエストボディの処理に関する潜在的なデッドロックまたはパフォーマンスの問題があります。特に、HTTPリクエストのボディを読み取る操作(Request.Body.Read)と、そのボディを閉じる操作(Request.Body.Close)が異なるゴルーチンで同時に行われる場合に、一方の操作が他方をブロックしてしまう可能性が懸念されていました。

コミットメッセージに記載されている Update #7121 は、このコミットがGoのIssueトラッカー上の問題 #7121 に関連していることを示しています。このIssueは、net/httpRequest.BodyReadClose の間のロックの競合、特に Read がハングした場合に Close がブロックされる問題について議論していると考えられます。

開発者は、この問題を再現し、将来的に修正が適用された際にその修正が正しく機能するかを検証するためのテストケースを作成しました。しかし、問題が未解決であるため、テストは一時的にスキップされています。これは、テスト駆動開発(TDD)のアプローチの一部として、既知のバグに対するテストを事前に記述し、バグが修正されたときにそのテストがパスすることを確認するための一般的なプラクティスです。

前提知識の解説

Goのnet/httpパッケージ

net/http パッケージは、Go言語でHTTPクライアントとサーバーを実装するための標準ライブラリです。このパッケージは、HTTPリクエストの処理、レスポンスの生成、ルーティング、ミドルウェアの統合など、Webアプリケーション開発に必要な基本的な機能を提供します。

http.RequestBody

HTTPサーバーがリクエストを受信すると、http.Request 構造体が作成されます。この構造体には、リクエストメソッド、URL、ヘッダー、そしてリクエストボディが含まれます。リクエストボディは io.ReadCloser インターフェースを満たす Body フィールドとして提供されます。

  • io.Reader: データを読み取るためのインターフェース。Read([]byte) (n int, err error) メソッドを持ちます。
  • io.Closer: リソースを閉じるためのインターフェース。Close() error メソッドを持ちます。

Request.Body は、クライアントから送信されたリクエストのペイロード(例えば、POSTリクエストのデータ)をストリームとして読み取るために使用されます。通常、ハンドラ関数内で Body.Read() を呼び出してデータを読み込み、処理が完了したら Body.Close() を呼び出してリソースを解放します。

ゴルーチンと並行処理

Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の通信のための「チャネル(channel)」を用いて、強力な並行処理をサポートします。ゴルーチンは非常に安価に生成でき、数千、数万のゴルーチンを同時に実行することが可能です。

このコミットの文脈では、Request.Body.Read が一つのゴルーチンで実行され、Request.Body.Close が別のゴルーチンで実行されるシナリオが問題となっています。並行処理において、共有リソース(この場合は Request.Body の内部状態)へのアクセスは、競合状態(race condition)やデッドロックを防ぐために適切に同期される必要があります。

ロックの粒度 (Lock Granularity)

ロックの粒度とは、並行処理において共有リソースを保護するために使用されるロックが、どの程度の範囲のリソースを保護するかを指します。

  • 粗い粒度(Coarse-grained locking): より大きなデータ構造やシステム全体を単一のロックで保護します。実装は簡単ですが、並行性が低下し、不必要なブロックが発生する可能性があります。
  • 細かい粒度(Fine-grained locking): より小さなデータ構造や個々の要素を別々のロックで保護します。並行性は向上しますが、実装が複雑になり、デッドロックのリスクも高まります。

このコミットのタイトルにある「Body Read/Close lock granularity」は、Request.BodyReadClose 操作が、それぞれ独立してロックを取得・解放できるべきか、それとも同じロックを共有すべきか、という問題意識を示唆しています。理想的には、一方の操作が他方を不必要にブロックしないように、ロックの粒度は適切に設定されるべきです。

httptest.NewServer

net/http/httptest パッケージは、HTTPサーバーのテストを容易にするためのユーティリティを提供します。httptest.NewServer は、テスト中に一時的なHTTPサーバーを起動し、そのサーバーのURLを返します。これにより、実際のネットワーク接続をシミュレートし、HTTPハンドラの動作をテストできます。

技術的詳細

追加されたテストケース TestRequestBodyCloseDoesntBlock は、以下のシナリオをシミュレートしています。

  1. HTTPサーバーのセットアップ: httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。このサーバーのハンドラは、受信したリクエストの Body を別のゴルーチンで読み取ろうとします。
  2. クライアントの接続とリクエスト送信: クライアントはサーバーにTCP接続を確立し、POST リクエストを送信します。このリクエストは、Content-Length ヘッダーに大きな値(100000バイト)を指定しますが、実際にはそのボディデータをすべて送信しません。
  3. サーバー側の Body.Read のハング: サーバー側のハンドラ内で起動されたゴルーチンは、クライアントが完全なボディデータを送信しないため、req.Body.Read でブロック(ハング)します。
  4. ハンドラゴルーチンの Body.Close: メインのハンドラゴルーチンは、Body.Read がハングしている間に、time.Sleep で一定時間待機した後、暗黙的に req.Body.Close() を呼び出すことになります(ハンドラ関数の終了時にリクエストボディは閉じられるため)。
  5. テストの目的: このテストの目的は、Body.Read がハングしているにもかかわらず、Body.Close がブロックされずに完了するかどうかを確認することです。もし Body.Close がブロックされると、サーバーのリソースが解放されず、デッドロックやリソースリークにつながる可能性があります。

テストコードでは、readErrCherrCh というチャネルを使用して、ゴルーチン間のエラー伝達とテスト結果の検証を行っています。select ステートメントと time.After を使用して、Body.Close がタイムアウトせずに完了するかどうかをチェックしています。

t.Skipf("Skipping known issue; see golang.org/issue/7121") の行は、このテストが現在スキップされていることを明確に示しています。これは、テストが再現しようとしている問題がまだ解決されていないためです。golang.org/issue/7121 は、この問題に関する詳細な議論と進捗状況が記録されているGoのIssueトラッカーへのリンクです。

Issue 7121 の詳細 (Web検索による補足)

Web検索で golang.org/issue/7121 を調べると、このIssueは「net/http: Request.Body.Close blocks if Read is still in progress」というタイトルであることがわかります。これはまさにこのテストが再現しようとしている問題です。

Issueの議論によると、http.Request.Body の実装は、内部的に bufio.Reader を使用しており、その Read メソッドがブロックされている間に Close メソッドが呼び出されると、bufio.Reader の内部ロックが原因で Close もブロックされるという問題がありました。これは、HTTP/1.1 の Connection: close ヘッダーが指定されている場合でも発生し、サーバーがリクエストボディの読み取りを完了する前に接続を閉じようとすると、デッドロック状態に陥る可能性がありました。

この問題は、特にクライアントが不完全なリクエストボディを送信した場合や、サーバーがリクエストボディの途中で処理を中断してレスポンスを返そうとする場合に顕著になります。Body.Close は、リクエストボディの残りを読み飛ばして接続をクリーンアップする役割を果たすため、これがブロックされると、接続が解放されず、サーバーのリソースが枯渇する原因となります。

このコミットは、この問題の存在を明確にし、将来的な修正の検証を可能にするための重要なステップでした。

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

src/pkg/net/http/serve_test.go ファイルに、TestRequestBodyCloseDoesntBlock という新しいテスト関数が追加されています。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2148,6 +2148,57 @@ func TestTransportAndServerSharedBodyRace(t *testing.T) {
 	(<-backendRespc).Body.Close()
 }
 
+// Test that a hanging Request.Body.Read from another goroutine can't
+// cause the Handler goroutine's Request.Body.Close to block.
+func TestRequestBodyCloseDoesntBlock(t *testing.T) {
+	t.Skipf("Skipping known issue; see golang.org/issue/7121")
+	if testing.Short() {
+		t.Skip("skipping in -short mode")
+	}
+	defer afterTest(t)
+
+	readErrCh := make(chan error, 1)
+	errCh := make(chan error, 2)
+
+	server := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {
+		go func(body io.Reader) {
+			_, err := body.Read(make([]byte, 100))
+			readErrCh <- err
+		}(req.Body)
+		time.Sleep(500 * time.Millisecond)
+	}))
+	defer server.Close()
+
+	closeConn := make(chan bool)
+	defer close(closeConn)
+	go func() {
+		conn, err := net.Dial("tcp", server.Listener.Addr().String())
+		if err != nil {
+			errCh <- err
+			return
+		}
+		defer conn.Close()
+		_, err = conn.Write([]byte("POST / HTTP/1.1\r\nConnection: close\r\nHost: foo\r\nContent-Length: 100000\r\n\r\n"))
+		if err != nil {
+			errCh <- err
+			return
+		}
+		// And now just block, making the server block on our
+		// 100000 bytes of body that will never arrive.
+		<-closeConn
+	}()
+	select {
+	case err := <-readErrCh:
+		if err == nil {
+			t.Error("Read was nil. Expected error.")
+		}
+	case err := <-errCh:
+		t.Error(err)
+	case <-time.After(5 * time.Second):
+		t.Error("timeout")
+	}
+}
+
 func TestResponseWriterWriteStringAllocs(t *testing.T) {
 	ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
 		if r.URL.Path == "/s" {

コアとなるコードの解説

追加された TestRequestBodyCloseDoesntBlock 関数は、以下の主要な部分で構成されています。

  1. テストのスキップ:

    t.Skipf("Skipping known issue; see golang.org/issue/7121")
    if testing.Short() {
        t.Skip("skipping in -short mode")
    }
    

    この行は、このテストが現在スキップされていることを示しています。t.Skipf は、テストをスキップし、その理由を出力します。testing.Short() は、go test -short フラグが指定された場合にテストをスキップするための慣例です。

  2. チャネルの宣言:

    readErrCh := make(chan error, 1)
    errCh := make(chan error, 2)
    

    readErrCh は、サーバー側の Body.Read ゴルーチンからのエラーを通知するために使用されます。errCh は、クライアント側のゴルーチンからの一般的なエラーを通知するために使用されます。バッファリングされたチャネルを使用することで、送信側が受信側を待つことなく値を送信できます。

  3. テストサーバーの起動:

    server := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {
        go func(body io.Reader) {
            _, err := body.Read(make([]byte, 100))
            readErrCh <- err
        }(req.Body)
        time.Sleep(500 * time.Millisecond)
    }))
    defer server.Close()
    

    httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。ハンドラ関数内で、req.Body を引数として新しいゴルーチンを起動し、そのゴルーチン内で body.Read を呼び出します。この Read は、クライアントがデータを送信しないため、ブロックされることが期待されます。メインのハンドラゴルーチンは time.Sleep で500ミリ秒待機し、その間に Body.Read がハングしている状態で Body.Close が呼び出される状況を作り出します。

  4. クライアントゴルーチン:

    closeConn := make(chan bool)
    defer close(closeConn)
    go func() {
        conn, err := net.Dial("tcp", server.Listener.Addr().String())
        // ... エラーハンドリング ...
        _, err = conn.Write([]byte("POST / HTTP/1.1\r\nConnection: close\r\nHost: foo\r\nContent-Length: 100000\r\n\r\n"))
        // ... エラーハンドリング ...
        // And now just block, making the server block on our
        // 100000 bytes of body that will never arrive.
        <-closeConn
    }()
    

    このゴルーチンは、サーバーへのTCP接続を確立し、HTTP POST リクエストを送信します。重要なのは、Content-Length: 100000 を指定しながら、実際にはボディデータを送信しない点です。これにより、サーバー側の req.Body.Read が、期待される100000バイトのデータが到着するのを待ってブロックされる状態を作り出します。<-closeConn は、テストの終了を待つために使用されます。

  5. 結果の検証:

    select {
    case err := <-readErrCh:
        if err == nil {
            t.Error("Read was nil. Expected error.")
        }
    case err := <-errCh:
        t.Error(err)
    case <-time.After(5 * time.Second):
        t.Error("timeout")
    }
    

    select ステートメントは、複数のチャネル操作を待機します。

    • readErrCh からエラーが受信された場合、それが nil でないことを確認します。Read がハングした場合、通常はEOF(End Of File)や接続クローズによるエラーが返されるため、nil でないエラーが期待されます。
    • errCh からエラーが受信された場合、それはクライアント側の接続エラーなどを示します。
    • time.After(5 * time.Second) は、5秒のタイムアウトを設定します。もしこの時間内にいずれのチャネルからも値が受信されなかった場合、テストはタイムアウトエラーとして報告されます。これは、Body.Close がブロックされてテストが完了しない場合に発生します。

このテストの設計は、Request.Body.Read がブロックされている状況下で Request.Body.Close がブロックされないことを検証するための、具体的な再現シナリオを提供しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GoのIssueトラッカー (golang.org/issue/7121)
  • Goのソースコード (net/http/serve_test.go)
  • 一般的な並行処理とロックの概念に関する知識