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

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

このコミットは、Go言語の net パッケージにおける TestAddFDReturnsError というテストが断続的に失敗する問題を修正します。具体的には、テスト内の競合状態を解消し、テストの信頼性を向上させています。

コミット

commit d244dd09f35b06909ef5590d3a2fde65bc1f0612
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Nov 28 10:08:59 2012 +1100

    net: fix intermittent TestAddFDReturnsError failure
    
    A fix similar to CL 6859043 was effective in resolving the intermittent failure.
    
    Fixes #4423.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6854102

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

https://github.com/golang/go/commit/d244dd09f35b06909ef5590d3a2fde65bc1f0612

元コミット内容

このコミットは、net パッケージのテスト TestAddFDReturnsError における断続的な失敗を修正することを目的としています。この問題は、テストの実行環境やタイミングによって結果が変動する、いわゆる「flaky test (不安定なテスト)」として認識されていました。コミットメッセージによると、CL 6859043 と同様の修正がこの断続的な失敗の解決に効果的であったとされています。この修正は、GoのIssue #4423 に関連しています。

変更の背景

TestAddFDReturnsError は、ネットワーク接続がクローズされた後にファイルディスクリプタ (FD) を追加しようとした場合に、適切なエラーが返されることを検証するテストです。しかし、このテストは断続的に失敗していました。これは、テストコード内に潜在的な競合状態 (race condition) が存在し、テストのセットアップと実行のタイミングが特定の順序で発生した場合にのみ問題が顕在化するためと考えられます。

具体的には、テスト内でリスナーが接続を Accept するゴルーチンと、クライアントが接続を Dial する処理の間に同期が取れていなかったことが原因である可能性が高いです。クライアントが接続を試みる前にリスナーが完全に準備できていない場合、Dial が失敗したり、意図しない動作を引き起こしたりする可能性がありました。このような不安定なテストは、CI/CDパイプラインの信頼性を低下させ、開発者が実際のバグとテストの不安定性を区別するのを困難にします。このコミットは、この不安定性を解消し、テストの信頼性を確保することを目的としています。

前提知識の解説

  • Goの net パッケージ: Go言語の標準ライブラリの一部で、TCP/UDPネットワーク通信、HTTPクライアント/サーバー、DNSルックアップなど、様々なネットワーク機能を提供します。
  • ListenAccept:
    • net.Listen(network, address string): 指定されたネットワークアドレスで新しいリスナーを作成します。TCPの場合、これはサーバーがクライアントからの接続を待機するためのソケットを開くことに相当します。
    • Listener.Accept(): リスナーがクライアントからの新しい接続を待機し、接続が確立されると、その接続を表す net.Conn インターフェースを返します。
  • DialDialTCP:
    • net.Dial(network, address string): 指定されたネットワークアドレスに接続を確立します。これはクライアントがサーバーに接続するために使用されます。
    • net.DialTCP(network string, laddr, raddr *TCPAddr): TCPネットワークに特化した接続確立関数で、ローカルアドレス (laddr) とリモートアドレス (raddr) を明示的に指定できます。
  • TCPListenerTCPConn:
    • net.TCPListener: TCPネットワーク接続をリッスンするためのリスナー型です。
    • net.TCPConn: TCPネットワーク接続を表す型です。
  • pollServer: Goの net パッケージ内部で使用される、I/O操作を非同期で処理するためのメカニズムです。ファイルディスクリプタ (FD) の準備状況を監視し、I/Oイベントが発生したときに通知します。ネットワーク接続がクローズされると、関連する pollServer もクローズされ、それ以降のI/O操作はエラーを返す必要があります。
  • 競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。テストにおいては、テストのセットアップと検証の間にタイミング依存性がある場合に発生し、断続的な失敗の原因となります。
  • チャネル (Channel): Go言語におけるゴルーチン間の通信と同期のためのプリミティブです。チャネルを通じて値を送受信することで、ゴルーチン間の安全なデータ交換と実行順序の調整が可能です。

技術的詳細

このコミットの技術的な核心は、TestAddFDReturnsError テストにおける競合状態の解消です。元のテストコードでは、以下の順序で処理が行われていました。

  1. リスナー (l) を作成し、ゴルーチン内で l.Accept() を開始。
  2. メインゴルーチンで Dial を使用してリスナーに接続を試みる。

このシーケンスでは、Dial が実行される時点で、ゴルーチン内の Accept がまだ完全に準備できていない(つまり、リスナーが接続を受け入れる準備ができていない)可能性があります。これにより、Dial が失敗したり、テストが意図しないパスを辿ったりして、断続的なテスト失敗につながっていました。

修正では、この競合状態を解消するために以下の変更が導入されました。

  1. リスナーの初期化の変更: Listen の代わりに newLocalListener(t).(*TCPListener) を使用しています。これはテストヘルパー関数であり、より堅牢なリスナーの初期化を提供し、テストの信頼性を高める可能性があります。
  2. Dial の変更: Dial の代わりに DialTCP を使用しています。これはより具体的なTCP接続確立関数であり、テストの意図を明確にします。
  3. チャネルによる同期: 最も重要な変更は、connected := make(chan bool) というチャネルを導入したことです。
    • リスナーの Accept ゴルーチン内で接続が確立された後 (c, err := ln.Accept() の後)、connected <- true を使用してチャネルに値を送信します。これは、接続が正常に受け入れられたことをメインゴルーチンに通知するシグナルとして機能します。
    • メインゴルーチンで DialTCP を呼び出した後、<-connected を使用してチャネルからの値の受信を待ちます。これにより、クライアントが接続を確立した後、リスナー側でその接続が Accept されるまでテストの実行がブロックされます。

このチャネルによる同期メカニズムにより、DialTCP が成功し、かつ Accept が完了して接続が確立されたことが保証されます。これにより、テストのセットアップにおけるタイミング依存性が排除され、競合状態が解消されます。

また、c.(*TCPConn).conn.fd.pollServer = psc.conn.fd.pollServer = ps に変更されています。これは、DialTCP が返す net.Conn の具体的な型が *net.TCPConn であることがより明確になったため、型アサーションが不要になったことを示唆しています。これはコードの簡潔化と可読性の向上に寄与します。

最後に、テストの最終的なエラー報告ロジックも修正されています。元のコードでは、t.Errorf の後に無条件で t.Error(err) が呼び出されており、これはテストが常にエラーを報告する原因となっていました。修正後は、t.Errorf("unexpected error: %v", err) の代わりに t.Error("unexpected error:", err) を使用し、エラーメッセージのフォーマットを改善し、冗長なエラー報告を避けています。

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

変更は src/pkg/net/fd_unix_test.go ファイルに集中しています。

--- a/src/pkg/net/fd_unix_test.go
+++ b/src/pkg/net/fd_unix_test.go
@@ -13,27 +13,26 @@ import (
 // Issue 3590. netFd.AddFD should return an error
 // from the underlying pollster rather than panicing.
 func TestAddFDReturnsError(t *testing.T) {
-	l, err := Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer l.Close()
-
+	ln := newLocalListener(t).(*TCPListener)
+	defer ln.Close()
+	connected := make(chan bool)
 	go func() {
 		for {
-			c, err := l.Accept()
+			c, err := ln.Accept()
 			if err != nil {
 				return
 			}
+			connected <- true
 			defer c.Close()
 		}
 	}()
 
-	c, err := Dial("tcp", l.Addr().String())
+	c, err := DialTCP("tcp", nil, ln.Addr().(*TCPAddr))
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer c.Close()
+	<-connected
 
 	// replace c's pollServer with a closed version.
 	ps, err := newPollServer()
@@ -41,7 +40,7 @@ func TestAddFDReturnsError(t *testing.T) {
 		t.Fatal(err)
 	}
 	ps.poll.Close()
-	c.(*TCPConn).conn.fd.pollServer = ps
+	c.conn.fd.pollServer = ps
 
 	var b [1]byte
 	_, err = c.Read(b[:])
@@ -56,5 +55,5 @@ func TestAddFDReturnsError(t *testing.T) {
 			}
 		}
 	}
-\tt.Error(err)\n+\tt.Error("unexpected error:", err)\n }\n

コアとなるコードの解説

  • ln := newLocalListener(t).(*TCPListener):
    • 元の l, err := Listen("tcp", "127.0.0.1:0") を置き換えています。
    • newLocalListener(t) はテスト用のヘルパー関数で、ローカルアドレスでTCPリスナーを安全に作成します。これにより、テストのセットアップがより堅牢になります。
    • .(*TCPListener) は、返されたリスナーが *net.TCPListener 型であることをアサートしています。
  • connected := make(chan bool):
    • bool 型のバッファなしチャネル connected を作成します。これは、リスナーが接続を受け入れたことをメインゴルーチンに通知するための同期プリミティブとして機能します。
  • connected <- true:
    • リスナーの Accept ゴルーチン内で、クライアント接続が正常に受け入れられた直後に実行されます。
    • これにより、connected チャネルに true の値が送信され、メインゴルーチンが待機している場合はその待機が解除されます。
  • c, err := DialTCP("tcp", nil, ln.Addr().(*TCPAddr)):
    • 元の c, err := Dial("tcp", l.Addr().String()) を置き換えています。
    • DialTCP はTCP接続に特化した関数で、より明示的な接続確立を行います。nil はローカルアドレスを指定しないことを意味し、ln.Addr().(*TCPAddr) はリスナーのアドレスを *TCPAddr 型にアサートしてリモートアドレスとして使用します。
  • <-connected:
    • DialTCP が呼び出された後、メインゴルーチンで実行されます。
    • connected チャネルから値を受信するまで、この行で実行がブロックされます。これにより、クライアントが接続を試みた後、リスナーがその接続を Accept するまでテストが進行しないことが保証され、競合状態が解消されます。
  • c.conn.fd.pollServer = ps:
    • 元の c.(*TCPConn).conn.fd.pollServer = ps を置き換えています。
    • DialTCP が返す c がすでに *TCPConn 型であることがコンパイラによって推論できるため、明示的な型アサーション (*TCPConn) が不要になりました。これはコードの簡潔化と可読性の向上に寄与します。
  • t.Error("unexpected error:", err):
    • 元の t.Error(err) を置き換えています。
    • これにより、エラーメッセージがより明確になり、テストが失敗した場合に何が問題であったかを理解しやすくなります。また、元のコードでは t.Errorf の後に無条件で t.Error(err) が呼び出されており、これはテストが常にエラーを報告する原因となっていましたが、この修正により、エラー報告がより適切に行われるようになります。

これらの変更により、テストのセットアップにおけるタイミング依存性が排除され、TestAddFDReturnsError が断続的に失敗する問題が解決されました。

関連リンク

  • Go Issue #4423: このコミットが修正したとされるGoのIssue。ただし、公開されているGoのIssueトラッカーでは直接関連する情報が見つからないため、内部的なIssue番号である可能性があります。
  • Go Change List (CL) 6854102: このコミットに関連するGoの変更リスト。
  • Go Change List (CL) 6859043: コミットメッセージで参照されている、同様の修正が効果的であったとされる変更リスト。

参考にした情報源リンク