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

[インデックス 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.goTestStressSurpriseServerCloses という新しいテスト関数を追加していますが、その直後に if true { ... return } というコードブロックを挿入することで、このテストを無効化しています。

無効化された TestStressSurpriseServerCloses テストは、http.Transport がサーバーからの予期せぬコネクション切断にどのように対処するかをストレス下で検証することを目的としています。

テストの主要なコンポーネントと動作は以下の通りです。

  1. テストサーバーのセットアップ:

    • httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。
    • このサーバーのハンドラは、レスポンスヘッダ(Content-Length, Content-Type)を設定し、ボディの一部("Hello")を書き込んだ後、Flusher インターフェースを使って即座にフラッシュします。
    • その後、Hijacker インターフェースを使って基盤となるTCPコネクションをハイジャックし、バッファをフラッシュした後にサーバー側からコネクションを即座にクローズします。これは、クライアントがまだレスポンスの残りを期待しているかもしれない状況で、サーバーが突然コネクションを切断するシナリオをシミュレートします。
  2. HTTPクライアントのセットアップ:

    • http.Transport のインスタンス tr を作成します。DisableKeepAlivesfalse に設定されており、Keep-Aliveが有効であることを示唆しています。
    • この Transport を使用して http.Client のインスタンス c を作成します。
  3. 並行リクエストの実行:

    • numClients (50) 個のゴルーチンを起動し、それぞれが reqsPerClient (250) 回のHTTP GETリクエストをテストサーバーに送信します。
    • 合計で 50 * 250 = 12500 回のリクエストが並行して送信されます。
    • 各リクエストが完了するたびに(成功または失敗にかかわらず)、activityc チャネルに true を送信します。
  4. デッドロック検出:

    • メインゴルーチンは、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 が、サーバーが予期せずコネクションをクローズする状況下で、多数の並行リクエストを処理する際の堅牢性をテストするために設計されています。

  1. テストの無効化:

    if true {
        t.Logf("known broken test; fix coming. Issue 2616")
        return
    }
    

    このブロックが、このコミットの主要な変更点です。if true は常に真であるため、テストが実行されるとすぐにこのブロックに入り、t.Logf でメッセージをログに出力し、return で関数を終了します。これにより、テストの残りのロジックは実行されず、テストはスキップされます。これは、テストが不安定であるか、またはデッドロックを引き起こす既知のバグを露呈するため、一時的に無効化されたことを示しています。コミットメッセージにあるように、Issue 2616 の「本当の修正」が別のCLで提供される予定であり、それまでの間、この不安定なテストを無効にしています。

  2. ショートモードでのスキップ:

    if testing.Short() {
        t.Logf("skipping test in short mode")
        return
    }
    

    これはGoのテストにおける一般的なパターンで、go test -short コマンドで実行された場合に、時間のかかるテストをスキップするためのものです。このテストはストレステストであるため、実行に時間がかかることが予想されます。

  3. テストサーバーのセットアップ:

    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 がこのような予期せぬ切断にどのように対処するかをテストするために重要です。
  4. クライアントのセットアップ:

    tr := &Transport{DisableKeepAlives: false}
    c := &Client{Transport: tr}
    
    • http.Transport のインスタンスを作成し、DisableKeepAlivesfalse に設定しています。これは、Keep-Aliveが有効であり、コネクションの再利用が試みられることを意味します。この設定は、デッドロックや競合状態が発生しやすい条件を作り出します。
    • この Transport を使用して http.Client を作成します。
  5. 並行リクエストの実行:

    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 チャネルにシグナルを送信します。
  6. デッドロックの監視:

    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 が多数の並行リクエストと予期せぬサーバー側のコネクション切断という複雑なシナリオにおいて、デッドロックに陥ることなく、すべてのリクエストを最終的に処理できるか(エラーになっても良い)を検証しようとしていました。テストが無効化されたのは、このシナリオでデッドロックが頻繁に発生し、テストが不安定であったためです。

関連リンク

参考にした情報源リンク