[インデックス 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ルックアップなど、様々なネットワーク機能を提供します。 Listen
とAccept
:net.Listen(network, address string)
: 指定されたネットワークアドレスで新しいリスナーを作成します。TCPの場合、これはサーバーがクライアントからの接続を待機するためのソケットを開くことに相当します。Listener.Accept()
: リスナーがクライアントからの新しい接続を待機し、接続が確立されると、その接続を表すnet.Conn
インターフェースを返します。
Dial
とDialTCP
:net.Dial(network, address string)
: 指定されたネットワークアドレスに接続を確立します。これはクライアントがサーバーに接続するために使用されます。net.DialTCP(network string, laddr, raddr *TCPAddr)
: TCPネットワークに特化した接続確立関数で、ローカルアドレス (laddr
) とリモートアドレス (raddr
) を明示的に指定できます。
TCPListener
とTCPConn
:net.TCPListener
: TCPネットワーク接続をリッスンするためのリスナー型です。net.TCPConn
: TCPネットワーク接続を表す型です。
pollServer
: Goのnet
パッケージ内部で使用される、I/O操作を非同期で処理するためのメカニズムです。ファイルディスクリプタ (FD) の準備状況を監視し、I/Oイベントが発生したときに通知します。ネットワーク接続がクローズされると、関連するpollServer
もクローズされ、それ以降のI/O操作はエラーを返す必要があります。- 競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。テストにおいては、テストのセットアップと検証の間にタイミング依存性がある場合に発生し、断続的な失敗の原因となります。
- チャネル (Channel): Go言語におけるゴルーチン間の通信と同期のためのプリミティブです。チャネルを通じて値を送受信することで、ゴルーチン間の安全なデータ交換と実行順序の調整が可能です。
技術的詳細
このコミットの技術的な核心は、TestAddFDReturnsError
テストにおける競合状態の解消です。元のテストコードでは、以下の順序で処理が行われていました。
- リスナー (
l
) を作成し、ゴルーチン内でl.Accept()
を開始。 - メインゴルーチンで
Dial
を使用してリスナーに接続を試みる。
このシーケンスでは、Dial
が実行される時点で、ゴルーチン内の Accept
がまだ完全に準備できていない(つまり、リスナーが接続を受け入れる準備ができていない)可能性があります。これにより、Dial
が失敗したり、テストが意図しないパスを辿ったりして、断続的なテスト失敗につながっていました。
修正では、この競合状態を解消するために以下の変更が導入されました。
- リスナーの初期化の変更:
Listen
の代わりにnewLocalListener(t).(*TCPListener)
を使用しています。これはテストヘルパー関数であり、より堅牢なリスナーの初期化を提供し、テストの信頼性を高める可能性があります。 Dial
の変更:Dial
の代わりにDialTCP
を使用しています。これはより具体的なTCP接続確立関数であり、テストの意図を明確にします。- チャネルによる同期: 最も重要な変更は、
connected := make(chan bool)
というチャネルを導入したことです。- リスナーの
Accept
ゴルーチン内で接続が確立された後 (c, err := ln.Accept()
の後)、connected <- true
を使用してチャネルに値を送信します。これは、接続が正常に受け入れられたことをメインゴルーチンに通知するシグナルとして機能します。 - メインゴルーチンで
DialTCP
を呼び出した後、<-connected
を使用してチャネルからの値の受信を待ちます。これにより、クライアントが接続を確立した後、リスナー側でその接続がAccept
されるまでテストの実行がブロックされます。
- リスナーの
このチャネルによる同期メカニズムにより、DialTCP
が成功し、かつ Accept
が完了して接続が確立されたことが保証されます。これにより、テストのセットアップにおけるタイミング依存性が排除され、競合状態が解消されます。
また、c.(*TCPConn).conn.fd.pollServer = ps
が c.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: コミットメッセージで参照されている、同様の修正が効果的であったとされる変更リスト。
参考にした情報源リンク
- https://github.com/golang/go/commit/d244dd09f35b06909ef5590d3a2fde65bc1f0612
- Go言語の
net
パッケージのドキュメント (Go公式ドキュメント) - Go言語のチャネルに関するドキュメント (Go公式ドキュメント)
- Go言語のテストに関するドキュメント (Go公式ドキュメント)