[インデックス 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.Body
の Read
および Close
メソッドのロックの粒度に関する無効化されたテストを追加します。これは、Issue #7121 に関連するものです。
変更の背景
この変更の背景には、Goの net/http
パッケージにおけるリクエストボディの処理に関する潜在的なデッドロックまたはパフォーマンスの問題があります。特に、HTTPリクエストのボディを読み取る操作(Request.Body.Read
)と、そのボディを閉じる操作(Request.Body.Close
)が異なるゴルーチンで同時に行われる場合に、一方の操作が他方をブロックしてしまう可能性が懸念されていました。
コミットメッセージに記載されている Update #7121
は、このコミットがGoのIssueトラッカー上の問題 #7121 に関連していることを示しています。このIssueは、net/http
の Request.Body
の Read
と Close
の間のロックの競合、特に Read
がハングした場合に Close
がブロックされる問題について議論していると考えられます。
開発者は、この問題を再現し、将来的に修正が適用された際にその修正が正しく機能するかを検証するためのテストケースを作成しました。しかし、問題が未解決であるため、テストは一時的にスキップされています。これは、テスト駆動開発(TDD)のアプローチの一部として、既知のバグに対するテストを事前に記述し、バグが修正されたときにそのテストがパスすることを確認するための一般的なプラクティスです。
前提知識の解説
Goのnet/httpパッケージ
net/http
パッケージは、Go言語でHTTPクライアントとサーバーを実装するための標準ライブラリです。このパッケージは、HTTPリクエストの処理、レスポンスの生成、ルーティング、ミドルウェアの統合など、Webアプリケーション開発に必要な基本的な機能を提供します。
http.Request
と Body
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.Body
の Read
と Close
操作が、それぞれ独立してロックを取得・解放できるべきか、それとも同じロックを共有すべきか、という問題意識を示唆しています。理想的には、一方の操作が他方を不必要にブロックしないように、ロックの粒度は適切に設定されるべきです。
httptest.NewServer
net/http/httptest
パッケージは、HTTPサーバーのテストを容易にするためのユーティリティを提供します。httptest.NewServer
は、テスト中に一時的なHTTPサーバーを起動し、そのサーバーのURLを返します。これにより、実際のネットワーク接続をシミュレートし、HTTPハンドラの動作をテストできます。
技術的詳細
追加されたテストケース TestRequestBodyCloseDoesntBlock
は、以下のシナリオをシミュレートしています。
- HTTPサーバーのセットアップ:
httptest.NewServer
を使用してテスト用のHTTPサーバーを起動します。このサーバーのハンドラは、受信したリクエストのBody
を別のゴルーチンで読み取ろうとします。 - クライアントの接続とリクエスト送信: クライアントはサーバーにTCP接続を確立し、
POST
リクエストを送信します。このリクエストは、Content-Length
ヘッダーに大きな値(100000バイト)を指定しますが、実際にはそのボディデータをすべて送信しません。 - サーバー側の
Body.Read
のハング: サーバー側のハンドラ内で起動されたゴルーチンは、クライアントが完全なボディデータを送信しないため、req.Body.Read
でブロック(ハング)します。 - ハンドラゴルーチンの
Body.Close
: メインのハンドラゴルーチンは、Body.Read
がハングしている間に、time.Sleep
で一定時間待機した後、暗黙的にreq.Body.Close()
を呼び出すことになります(ハンドラ関数の終了時にリクエストボディは閉じられるため)。 - テストの目的: このテストの目的は、
Body.Read
がハングしているにもかかわらず、Body.Close
がブロックされずに完了するかどうかを確認することです。もしBody.Close
がブロックされると、サーバーのリソースが解放されず、デッドロックやリソースリークにつながる可能性があります。
テストコードでは、readErrCh
と errCh
というチャネルを使用して、ゴルーチン間のエラー伝達とテスト結果の検証を行っています。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
関数は、以下の主要な部分で構成されています。
-
テストのスキップ:
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
フラグが指定された場合にテストをスキップするための慣例です。 -
チャネルの宣言:
readErrCh := make(chan error, 1) errCh := make(chan error, 2)
readErrCh
は、サーバー側のBody.Read
ゴルーチンからのエラーを通知するために使用されます。errCh
は、クライアント側のゴルーチンからの一般的なエラーを通知するために使用されます。バッファリングされたチャネルを使用することで、送信側が受信側を待つことなく値を送信できます。 -
テストサーバーの起動:
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
が呼び出される状況を作り出します。 -
クライアントゴルーチン:
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
は、テストの終了を待つために使用されます。 -
結果の検証:
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 Issue 7121: https://golang.org/issue/7121
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
io
パッケージドキュメント: https://pkg.go.dev/io - Go
httptest
パッケージドキュメント: https://pkg.go.dev/net/http/httptest
参考にした情報源リンク
- Go言語の公式ドキュメント
- GoのIssueトラッカー (golang.org/issue/7121)
- Goのソースコード (net/http/serve_test.go)
- 一般的な並行処理とロックの概念に関する知識