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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージ内のTestTCPConcurrentAcceptテストの不安定性(flakiness)を解消することを目的としています。特定のプラットフォームにおいて、TCPのアドレスとポートの再利用に関する挙動が原因でテストが失敗することがあったため、テストのロジックを修正し、一時的な接続失敗を許容することで、テストの信頼性を向上させています。

コミット

net: deflake TestTCPConcurrentAccept

Some platform that implements inp_localgroup-like shared internet
protocol control block group looks a bit sensitive about transport
layer protocol's address:port reuse. Sometimes it rejects a TCP SYN
packet using TCP RST, and sometimes silence.

For now, until test case refactoring, we admit few Dial failures on
TestTCPConcurrentAccept as a workaround.

Update #7400
Update #7541

LGTM=jsing
R=jsing
CC=golang-codereviews
https://golang.org/cl/75920043

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

https://github.com/golang/go/commit/4f1aecf2c408da2ddf6fd2b4542b9fe1d239c5e3

元コミット内容

net: deflake TestTCPConcurrentAccept

Some platform that implements inp_localgroup-like shared internet
protocol control block group looks a bit sensitive about transport
layer protocol's address:port reuse. Sometimes it rejects a TCP SYN
packet using TCP RST, and sometimes silence.

For now, until test case refactoring, we admit few Dial failures on
TestTCPConcurrentAccept as a workaround.

Update #7400
Update #7541

LGTM=jsing
R=jsing
CC=golang-codereviews
https://golang.org/cl/75920043

変更の背景

TestTCPConcurrentAcceptは、複数のクライアントが同時にTCP接続を確立しようとするシナリオをテストするものです。しかし、一部のオペレーティングシステム(特にSolarisなど、コミットメッセージで言及されているinp_localgroupのような共有インターネットプロトコル制御ブロックグループを実装しているプラットフォーム)では、トランスポート層プロトコルのアドレスとポートの再利用に関して非常に厳格な挙動を示すことがありました。

具体的には、テスト中に短期間で大量のTCP接続が試行され、ポートがすぐに再利用されるような状況で、OSがTCP SYNパケットをRST(リセット)で拒否したり、あるいは単にサイレントに破棄したりすることがありました。これにより、テストが本来のロジックとは関係なく、環境依存のネットワーク挙動によって不安定に失敗する(flaky test)問題が発生していました。

このコミットは、このような環境依存の不安定性を解消し、テストの信頼性を向上させることを目的としています。根本的なテストケースのリファクタリングが行われるまでの暫定的な対策として、少数のDial(接続試行)失敗を許容するロジックが導入されました。コミットメッセージで参照されている#7400#7541は、この問題に関連するGoのイシュートラッカーの項目であると考えられます。

前提知識の解説

  • TCP (Transmission Control Protocol): インターネットで広く使われる信頼性の高いコネクション指向のプロトコルです。データの順序保証、再送制御、フロー制御などを行います。
  • TCP SYN/RST:
    • SYN (Synchronize): TCP接続を確立する際の最初のパケットです。クライアントがサーバーに接続要求を送る際に使用します。
    • RST (Reset): TCP接続を強制的にリセットするパケットです。通常、エラーが発生した場合や、接続要求が拒否された場合などに送信されます。
  • アドレスとポートの再利用: TCPソケットを閉じた後、そのソケットが使用していたローカルアドレスとポートのペアは、すぐに再利用できない場合があります。これは、TIME_WAIT状態などのTCPプロトコルの特性によるものです。しかし、SO_REUSEADDRSO_REUSEPORTといったソケットオプションを使用することで、特定の条件下でアドレスとポートの即時再利用が可能になります。一部のOSでは、これらのオプションの挙動や、短期間での大量のソケット操作に対する内部的な制限が厳しく、問題を引き起こすことがあります。
  • netパッケージ (Go言語): Go言語の標準ライブラリで、ネットワークプログラミングのための基本的なインターフェースを提供します。TCP/UDP接続の確立、リスニング、データの送受信など、様々なネットワーク操作が可能です。
  • TestTCPConcurrentAccept: このテストは、Goのnetパッケージが複数の同時接続要求を適切に処理できるかを確認します。具体的には、net.Listenでリスナーを起動し、複数のゴルーチンから同時にnet.Dialを試行し、リスナー側でAcceptが正しく行われるかを検証します。
  • Flaky Test (不安定なテスト): 実行するたびに成功したり失敗したりするテストのことです。コードのバグが原因ではなく、環境要因(ネットワークの遅延、リソースの競合、OSのスケジューリングなど)やテストのロジックの不備によって発生することが多いです。Flaky TestはCI/CDパイプラインの信頼性を損ない、開発者の生産性を低下させます。
  • t.Skip() (Go Testing): Goのテストフレームワークで、特定の条件が満たされた場合にテストの実行をスキップするために使用されます。通常、特定のOSやアーキテクチャに依存するテストで、その環境以外では実行しない場合に利用されます。
  • t.Fatalf() (Go Testing): テストが致命的なエラーに遭遇した場合に呼び出され、テストを即座に失敗させ、そのテスト関数を終了します。

技術的詳細

このコミットの技術的な変更点は、TestTCPConcurrentAcceptテストのロジックを、特定のプラットフォームでのネットワーク挙動の厳格さに対してより寛容にする点にあります。

  1. Solarisでのスキップ条件の削除: 変更前は、Solaris OS上でこのテストが実行される場合、t.Skip("skipping on Solaris, see issue 7400")によってテスト全体がスキップされていました。これは、Solaris環境での既知の問題(Issue 7400)に対応するための暫定的な措置でした。今回のコミットでは、テストロジックの改善により、このスキップ条件が削除され、Solarisでもテストが実行されるようになりました。

  2. Dial試行の失敗許容: 変更の核心は、Dialの失敗を即座にt.Fatalfでテストを終了させるのではなく、一定数の失敗を許容するようにした点です。

    • attempts := 10 * N:テストの試行回数を定義しています。Nはテストの並行度合いを示す変数です。
    • fails := 0Dialの失敗回数をカウントするための変数が導入されました。
    • d := &Dialer{Timeout: 200 * time.Millisecond}net.Dialの代わりに、タイムアウトを設定したDialerが使用されるようになりました。これにより、接続試行がハングアップするのを防ぎ、一定時間内に接続が確立できない場合はエラーを返すようになります。
    • ループ内でd.Dialを呼び出し、エラーが発生した場合はfailsをインクリメントします。成功した場合は、確立された接続をすぐにc.Close()で閉じます。
    • ループ終了後、if fails > attempts/9という条件で、失敗回数が総試行回数の約11%(1/9)を超えた場合にのみt.Fatalfでテストを失敗させます。これは、Issue 7400と7541で議論された内容に基づいています。
    • if fails > 0の場合、t.Logf("# of failed Dials: %v", fails)で失敗回数をログに出力します。これにより、テストが成功した場合でも、内部的に接続失敗が発生していたことを確認できます。

この変更により、一時的なネットワークの不安定性やOSのポート再利用に関する厳格な挙動によって発生する少数のDial失敗が、テスト全体の失敗に直結しなくなりました。テストはより堅牢になり、真のバグによってのみ失敗する可能性が高まります。

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

src/pkg/net/tcp_test.go ファイルが変更されています。

--- a/src/pkg/net/tcp_test.go
+++ b/src/pkg/net/tcp_test.go
@@ -445,9 +445,6 @@ func TestIPv6LinkLocalUnicastTCP(t *testing.T) {
 }
 
 func TestTCPConcurrentAccept(t *testing.T) {
-	if runtime.GOOS == "solaris" {
-		t.Skip("skipping on Solaris, see issue 7400")
-	}
 	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
 	ln, err := Listen("tcp", "127.0.0.1:0")
 	if err != nil {
@@ -468,15 +465,25 @@ func TestTCPConcurrentAccept(t *testing.T) {
 			wg.Done()
 		}()
 	}
-	for i := 0; i < 10*N; i++ {
-		c, err := Dial("tcp", ln.Addr().String())
+	attempts := 10 * N
+	fails := 0
+	d := &Dialer{Timeout: 200 * time.Millisecond}
+	for i := 0; i < attempts; i++ {
+		c, err := d.Dial("tcp", ln.Addr().String())
 		if err != nil {
-			t.Fatalf("Dial failed: %v", err)
+			fails++
+		} else {
+			c.Close()
 		}
-		c.Close()
 	}
 	ln.Close()
 	wg.Wait()
+	if fails > attempts/9 { // see issues 7400 and 7541
+		t.Fatalf("too many Dial failed: %v", fails)
+	}
+	if fails > 0 {
+		t.Logf("# of failed Dials: %v", fails)
+	}
 }
 
 func TestTCPReadWriteMallocs(t *testing.T) {

コアとなるコードの解説

変更されたTestTCPConcurrentAccept関数は、以下の点でテストの堅牢性を高めています。

  1. Solaris固有のスキップの削除: if runtime.GOOS == "solaris" { t.Skip(...) } の行が削除されました。これは、後続の変更によってSolaris環境でのテストの不安定性が解消されたため、もはやテストをスキップする必要がなくなったことを意味します。これにより、より多くの環境でこのテストが実行されるようになり、カバレッジが向上します。

  2. Dial試行の柔軟な処理: 変更前は、Dialが一度でも失敗するとt.Fatalfが呼び出され、テストが即座に終了していました。これは、一時的なネットワークの問題やOSの挙動に起因する接続失敗であっても、テスト全体が失敗してしまう原因となっていました。 変更後は、以下の新しいロジックが導入されました。

    • attempts := 10 * N: 接続試行の総回数を定義します。
    • fails := 0: 接続失敗のカウンターを初期化します。
    • d := &Dialer{Timeout: 200 * time.Millisecond}: net.Dialの代わりに、カスタムのDialerを使用します。このDialerは200ミリ秒のタイムアウトを設定しており、接続試行が長時間ブロックされることを防ぎます。
    • forループ内でd.Dialを呼び出し、接続が成功した場合はc.Close()で即座に接続を閉じ、リソースを解放します。接続が失敗した場合は、failsカウンターをインクリメントします。
    • ループが終了した後、if fails > attempts/9という条件で、失敗回数が総試行回数の9分の1(約11%)を超えているかをチェックします。この閾値は、過去の経験や関連するイシュー(#7400, #7541)に基づいて設定された、許容できる一時的な失敗の数を示しています。もし失敗がこの閾値を超えた場合のみ、t.Fatalfでテストを失敗させます。
    • if fails > 0の場合、t.Logf("# of failed Dials: %v", fails)で、テストが成功した場合でも発生した接続失敗の数をログに出力します。これは、テストのデバッグや、潜在的な問題の監視に役立ちます。

この一連の変更により、TestTCPConcurrentAcceptは、特定のプラットフォームで発生する可能性のある一時的なネットワークの厳格な挙動に対して、より耐性を持つようになりました。これにより、テストの信頼性が向上し、開発者はテストの失敗が真のバグによるものなのか、それとも環境的な要因によるものなのかをより正確に判断できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語のテストに関する一般的な情報
  • TCPプロトコル、SYN/RST、アドレス/ポート再利用に関する一般的なネットワーク知識
  • Go言語のnetパッケージのドキュメント
  • Goイシュートラッカーでの関連イシュー(#7400, #7541)に関する議論(直接的な検索結果は得られませんでしたが、コミットメッセージからの推測)