[インデックス 11388] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内の transport_test.go
ファイルに対する変更です。具体的には、HTTPトランスポートにおける競合状態(race condition)やデッドロックのバグを検出するためのテストを一時的に無効化しています。
コミット
commit cf09a9d3bfbdf82ba67419b7efbf188651786271
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jan 25 12:31:06 2012 -0800
net/http: disabled test for Transport race / deadlock bug
The real fix for Issue 2616 is in
https://golang.org/cl/5532057, to be submitted
following this CL, without the test there which doesn't work
reliably. This one seems to.
R=rsc
CC=golang-dev
https://golang.org/cl/5569063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cf09a9d3bfbdf82ba67419b7efbf188651786271
元コミット内容
net/http: disabled test for Transport race / deadlock bug
このコミットは、net/http
パッケージにおける Transport
の競合状態またはデッドロックのバグを検出するためのテストを無効化するものです。コミットメッセージによると、Issue 2616 の根本的な修正は別の変更リスト (CL 5532057) で行われ、このコミットはその修正に先立って、信頼性の低いテストを一時的に無効にするためのものです。
変更の背景
このコミットの背景には、Go言語の net/http
パッケージにおける Transport
のデッドロック問題、具体的には Issue 2616 が存在します。http.Transport
は、HTTPクライアントがネットワーク接続を管理し、リクエストを送信し、レスポンスを受信する際の基盤となるコンポーネントです。特に、コネクションの再利用(Keep-Alive)や並行リクエストの処理において、複雑な競合状態やデッドロックが発生する可能性があります。
コミットメッセージによると、このテストは GOMAXPROCS=100
のような高い並行度設定で実行すると、信頼性高く失敗することが示唆されています。これは、複数のゴルーチンが同時にHTTPリクエストを処理しようとする際に、Transport
内部の共有リソース(例えば、コネクションプール)へのアクセスが適切に同期されていない場合にデッドロックが発生する可能性を示しています。
このコミット自体は問題の修正ではなく、むしろ問題を示すテストを一時的に無効化するものです。これは、より包括的で信頼性の高い修正(CL 5532057)が別途準備されており、その修正が適用されるまでの間、不安定なテストがCI/CDパイプラインを妨げないようにするための措置と考えられます。つまり、テストが不安定であるため、まずテストを無効化し、その後に根本的な修正を適用するという開発フローの一部です。
前提知識の解説
Go言語の net/http
パッケージ
Go言語の net/http
パッケージは、HTTPクライアントとサーバーを実装するための強力な機能を提供します。
http.Client
: HTTPリクエストを送信するための高レベルなインターフェースを提供します。通常、Get
,Post
,Do
などのメソッドを通じて使用されます。http.Transport
:http.Client
の基盤となるコンポーネントで、実際のネットワーク接続の確立、管理、再利用(Keep-Alive)、プロキシの処理、TLSハンドシェイクなどを担当します。複数のリクエスト間でコネクションを効率的に再利用することで、パフォーマンスを向上させます。- Keep-Alive: HTTP/1.1の機能で、単一のTCPコネクション上で複数のHTTPリクエスト/レスポンスをやり取りすることを可能にします。これにより、コネクション確立のオーバーヘッドを削減し、レイテンシを低減します。
httptest.NewServer
: テスト目的でHTTPサーバーを簡単に起動するためのユーティリティ関数です。実際のネットワークポートをリッスンし、テスト対象のHTTPクライアントが接続できるエンドポイントを提供します。http.ResponseWriter
: HTTPレスポンスを書き込むためのインターフェースです。http.Request
: 受信したHTTPリクエストを表す構造体です。http.Hijacker
インターフェース:http.ResponseWriter
がこのインターフェースを実装している場合、ハンドラは基盤となるTCPコネクションを「ハイジャック」し、HTTPサーバーの通常の処理フローから独立して直接コネクションを操作できます。これは、WebSocketのようなプロトコルや、今回のテストのようにサーバーが意図的にコネクションを早期にクローズするシナリオで有用です。http.Flusher
インターフェース:http.ResponseWriter
がこのインターフェースを実装している場合、Flush()
メソッドを呼び出すことで、バッファリングされたレスポンスデータをクライアントに即座に送信できます。
競合状態(Race Condition)とデッドロック(Deadlock)
- 競合状態: 複数の並行プロセスやゴルーチンが共有リソースにアクセスし、そのアクセス順序によって結果が非決定的に変わる状況を指します。予期せぬ動作やバグの原因となります。
- デッドロック: 複数のプロセスやゴルーチンが互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態です。システムが応答しなくなる原因となります。
http.Transport
のようなコネクションプールを管理するコンポーネントでは、コネクションの取得と解放のロジックに不備があるとデッドロックが発生しやすいです。
GOMAXPROCS
GOMAXPROCS
環境変数は、Goランタイムが同時に実行できるOSスレッドの最大数を制御します。この値が高いほど、Goスケジューラはより多くのゴルーチンを並行して実行しようとします。これにより、並行処理における競合状態やデッドロックの問題が顕在化しやすくなります。
技術的詳細
このコミットは、src/pkg/net/http/transport_test.go
に TestStressSurpriseServerCloses
という新しいテスト関数を追加していますが、その直後に if true { ... return }
というコードブロックを挿入することで、このテストを無効化しています。
無効化された TestStressSurpriseServerCloses
テストは、http.Transport
がサーバーからの予期せぬコネクション切断にどのように対処するかをストレス下で検証することを目的としています。
テストの主要なコンポーネントと動作は以下の通りです。
-
テストサーバーのセットアップ:
httptest.NewServer
を使用してテスト用のHTTPサーバーを起動します。- このサーバーのハンドラは、レスポンスヘッダ(
Content-Length
,Content-Type
)を設定し、ボディの一部("Hello")を書き込んだ後、Flusher
インターフェースを使って即座にフラッシュします。 - その後、
Hijacker
インターフェースを使って基盤となるTCPコネクションをハイジャックし、バッファをフラッシュした後にサーバー側からコネクションを即座にクローズします。これは、クライアントがまだレスポンスの残りを期待しているかもしれない状況で、サーバーが突然コネクションを切断するシナリオをシミュレートします。
-
HTTPクライアントのセットアップ:
http.Transport
のインスタンスtr
を作成します。DisableKeepAlives
はfalse
に設定されており、Keep-Aliveが有効であることを示唆しています。- この
Transport
を使用してhttp.Client
のインスタンスc
を作成します。
-
並行リクエストの実行:
numClients
(50) 個のゴルーチンを起動し、それぞれがreqsPerClient
(250) 回のHTTP GETリクエストをテストサーバーに送信します。- 合計で
50 * 250 = 12500
回のリクエストが並行して送信されます。 - 各リクエストが完了するたびに(成功または失敗にかかわらず)、
activityc
チャネルにtrue
を送信します。
-
デッドロック検出:
- メインゴルーチンは、
numClients * reqsPerClient
回のactivityc
からの受信を待ちます。 select
ステートメントとtime.After(5 * time.Second)
を使用してタイムアウトを監視します。もし5秒間HTTPクライアントのアクティビティがなければ、デッドロックが発生したと判断し、テストを失敗させます。
- メインゴルーチンは、
このテストは、http.Transport
が多数の並行リクエストとサーバーからの予期せぬコネクション切断というストレス条件下で、デッドロックに陥ることなく適切にリソースを解放し、処理を継続できるかを検証しようとしています。コミットメッセージにあるように、このテストは GOMAXPROCS
を高く設定すると信頼性高く失敗したため、Transport
内部にデッドロックを引き起こすバグが存在したことを示唆しています。
コアとなるコードの変更箇所
変更は src/pkg/net/http/transport_test.go
ファイルに集中しています。
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -304,6 +304,66 @@ func TestTransportServerClosingUnexpectedly(t *testing.T) {
}
}
+// Test for http://golang.org/issue/2616 (appropriate issue number)
+// This fails pretty reliably with GOMAXPROCS=100 or something high.
+func TestStressSurpriseServerCloses(t *testing.T) {
+ if true {
+ t.Logf("known broken test; fix coming. Issue 2616")
+ return
+ }
+ if testing.Short() {
+ t.Logf("skipping test in short mode")
+ return
+ }
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ w.Header().Set("Content-Length", "5")
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte("Hello"))
+ w.(Flusher).Flush()
+ conn, buf, _ := w.(Hijacker).Hijack()
+ buf.Flush()
+ conn.Close()
+ }))
+ defer ts.Close()
+
+ tr := &Transport{DisableKeepAlives: false}
+ c := &Client{Transport: tr}
+
+ // Do a bunch of traffic from different goroutines. Send to activityc
+ // after each request completes, regardless of whether it failed.
+ const (
+ numClients = 50
+ reqsPerClient = 250
+ )
+ activityc := make(chan bool)
+ for i := 0; i < numClients; i++ {
+ go func() {
+ for i := 0; i < reqsPerClient; i++ {
+ res, err := c.Get(ts.URL)
+ if err == nil {
+ // We expect errors since the server is
+ // hanging up on us after telling us to
+ // send more requests, so we don't
+ // actually care what the error is.
+ // But we want to close the body in cases
+ // where we won the race.
+ res.Body.Close()
+ }
+ activityc <- true
+ }
+ }()
+ }
+
+ // Make sure all the request come back, one way or another.
+ for i := 0; i < numClients*reqsPerClient; i++ {
+ select {
+ case <-activityc:
+ case <-time.After(5 * time.Second):
+ t.Fatalf("presumed deadlock; no HTTP client activity seen in awhile")
+ }
+ }
+}
+
// TestTransportHeadResponses verifies that we deal with Content-Lengths
// with no bodies properly
func TestTransportHeadResponses(t *testing.T) {
この差分は、TestStressSurpriseServerCloses
という新しいテスト関数が追加されたことを示しています。しかし、このテスト関数の冒頭に以下のコードが追加されています。
if true {
t.Logf("known broken test; fix coming. Issue 2616")
return
}
この if true
ブロックにより、テスト関数が実行されるとすぐにログメッセージが出力され、関数がリターンするため、テストの残りの部分は実行されません。これにより、このテストは実質的に無効化されています。
コアとなるコードの解説
追加された TestStressSurpriseServerCloses
関数は、net/http
パッケージの Transport
が、サーバーが予期せずコネクションをクローズする状況下で、多数の並行リクエストを処理する際の堅牢性をテストするために設計されています。
-
テストの無効化:
if true { t.Logf("known broken test; fix coming. Issue 2616") return }
このブロックが、このコミットの主要な変更点です。
if true
は常に真であるため、テストが実行されるとすぐにこのブロックに入り、t.Logf
でメッセージをログに出力し、return
で関数を終了します。これにより、テストの残りのロジックは実行されず、テストはスキップされます。これは、テストが不安定であるか、またはデッドロックを引き起こす既知のバグを露呈するため、一時的に無効化されたことを示しています。コミットメッセージにあるように、Issue 2616 の「本当の修正」が別のCLで提供される予定であり、それまでの間、この不安定なテストを無効にしています。 -
ショートモードでのスキップ:
if testing.Short() { t.Logf("skipping test in short mode") return }
これはGoのテストにおける一般的なパターンで、
go test -short
コマンドで実行された場合に、時間のかかるテストをスキップするためのものです。このテストはストレステストであるため、実行に時間がかかることが予想されます。 -
テストサーバーのセットアップ:
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { w.Header().Set("Content-Length", "5") w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Hello")) w.(Flusher).Flush() conn, buf, _ := w.(Hijacker).Hijack() buf.Flush() conn.Close() })) defer ts.Close()
httptest.NewServer
は、テスト用のHTTPサーバーを起動します。- ハンドラ関数内で、レスポンスヘッダを設定し、"Hello"というボディの一部を書き込みます。
w.(Flusher).Flush()
は、バッファリングされたデータをクライアントに即座に送信します。これにより、クライアントはレスポンスの一部を受け取ったと認識します。w.(Hijacker).Hijack()
は、HTTPサーバーの通常の処理から基盤となるTCPコネクションを奪い取ります。これにより、ハンドラはコネクションを直接制御できます。conn.Close()
は、サーバー側からコネクションを即座にクローズします。これは、クライアントがまだレスポンスの残りを期待しているかもしれない状況で、サーバーが突然コネクションを切断するシナリオをシミュレートします。この動作は、http.Transport
がこのような予期せぬ切断にどのように対処するかをテストするために重要です。
-
クライアントのセットアップ:
tr := &Transport{DisableKeepAlives: false} c := &Client{Transport: tr}
http.Transport
のインスタンスを作成し、DisableKeepAlives
をfalse
に設定しています。これは、Keep-Aliveが有効であり、コネクションの再利用が試みられることを意味します。この設定は、デッドロックや競合状態が発生しやすい条件を作り出します。- この
Transport
を使用してhttp.Client
を作成します。
-
並行リクエストの実行:
const ( numClients = 50 reqsPerClient = 250 ) activityc := make(chan bool) for i := 0; i < numClients; i++ { go func() { for i := 0; i < reqsPerClient; i++ { res, err := c.Get(ts.URL) if err == nil { res.Body.Close() } activityc <- true } }() }
numClients
(50) 個のゴルーチンを起動し、それぞれがreqsPerClient
(250) 回のHTTP GETリクエストをテストサーバーに送信します。- 各リクエストは
c.Get(ts.URL)
を呼び出します。サーバーがコネクションを突然クローズするため、多くのリクエストでエラーが発生することが予想されます。 - エラーが発生しなかった場合(つまり、クライアントがレスポンスを正常に受け取れた場合)、
res.Body.Close()
を呼び出してレスポンスボディを閉じます。これは、リソースリークを防ぐために重要です。 - 各リクエストの完了後(成功または失敗にかかわらず)、
activityc
チャネルにシグナルを送信します。
-
デッドロックの監視:
for i := 0; i < numClients*reqsPerClient; i++ { select { case <-activityc: case <-time.After(5 * time.Second): t.Fatalf("presumed deadlock; no HTTP client activity seen in awhile") } }
- このループは、すべてのリクエスト(
numClients * reqsPerClient
回)が完了するのを待ちます。 select
ステートメントは、activityc
からのシグナルを待つか、または5秒間のタイムアウトを待ちます。- もし5秒以内に
activityc
からのシグナルがなければ、それはHTTPクライアントのアクティビティが停止したことを意味し、デッドロックが発生したと見なしてテストを失敗させます。
- このループは、すべてのリクエスト(
このテストは、http.Transport
が多数の並行リクエストと予期せぬサーバー側のコネクション切断という複雑なシナリオにおいて、デッドロックに陥ることなく、すべてのリクエストを最終的に処理できるか(エラーになっても良い)を検証しようとしていました。テストが無効化されたのは、このシナリオでデッドロックが頻繁に発生し、テストが不安定であったためです。
関連リンク
- Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
httptest
パッケージドキュメント: https://pkg.go.dev/net/http/httptest - Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing
参考にした情報源リンク
- Go Issue 2616:
net/http: Transport deadlock
(Goの公式Issueトラッカー): https://go.dev/issue/2616 - Go Change List 5532057 (Issue 2616の修正): https://golang.org/cl/5532057 (このCLは、このコミットのメッセージで言及されている「本当の修正」です。)
- Go Change List 5569063 (このコミット自体): https://golang.org/cl/5569063