[インデックス 15579] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージ内のテストの信頼性(flakiness)を向上させるための変更です。具体的には、ネットワーク接続のタイムアウト設定に関するテストにおいて、短すぎるタイムアウト値が原因でテストが不安定になる問題を解決しています。
コミット
commit 9744c0e175cfaf45d7fc7bdfad10618e1ba69cd6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 4 11:55:27 2013 -0800
net: make some tests less flaky
Fixes #4969
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7456049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9744c0e175cfaf45d7fc7bdfad10618e1ba69cd6
元コミット内容
このコミットの元の内容は、net
パッケージのテストにおけるタイムアウト設定を修正し、テストの不安定性(flakiness)を解消することです。具体的には、SetDeadline
, SetReadDeadline
, SetWriteDeadline
といったメソッドに設定されるタイムアウト値を、従来の100 * time.Millisecond
から10 * time.Second
へと延長しています。これにより、テスト実行環境の負荷やスケジューリングのばらつきによって発生していた、意図しないタイムアウトエラーを防ぎ、テストがより安定してパスするように改善されています。
変更の背景
この変更の背景には、Go言語のnet
パッケージにおけるテストが、特定の条件下で不安定になるという問題がありました。コミットメッセージにFixes #4969
とあることから、GoのIssueトラッカーで報告された問題4969が直接的な原因であることがわかります。
Issue #4969("net: TestConnAndListener is flaky")では、net
パッケージのTestConnAndListener
テストが、特に高負荷な環境やCI(継続的インテグレーション)システム上で、ランダムに失敗することが報告されていました。この失敗は、テスト内で設定されている非常に短いタイムアウト(100ミリ秒)が原因で発生していました。ネットワーク操作は、OSのスケジューリング、ネットワークの遅延、CPUの負荷など、様々な外部要因によって完了までに要する時間が変動します。100ミリ秒という短いタイムアウトでは、これらの変動に対応しきれず、正しく動作しているはずのネットワーク操作がタイムアウトとして扱われ、テストが失敗してしまうことがありました。
開発者は、この不安定なテストがCIパイプラインの信頼性を損ない、実際のバグではないにもかかわらず開発者の時間を浪費するという問題意識を持っていました。そのため、テストの目的が「タイムアウト機能そのもののテスト」ではなく、「SetFooDeadline
メソッドが呼ばれても実装が壊れないことの確認」であるならば、タイムアウト値を十分に長く設定することで、テストの安定性を確保する必要がありました。
前提知識の解説
Go言語のnet
パッケージ
Go言語のnet
パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うためのインターフェースや実装が含まれています。
net.Conn
インターフェース: ネットワーク接続の一般的なインターフェースを定義します。Read
,Write
,Close
,LocalAddr
,RemoteAddr
などのメソッドを持ちます。net.Listener
インターフェース: ネットワーク接続をリッスンするためのインターフェースを定義します。Accept
メソッドを持ち、新しい接続を受け入れます。SetDeadline
,SetReadDeadline
,SetWriteDeadline
: これらのメソッドは、net.Conn
インターフェース(およびその具体的な実装であるTCPConn
,UDPConn
など)に定義されており、ネットワーク操作のタイムアウトを設定するために使用されます。SetDeadline(t time.Time)
: 以降のすべての読み書き操作に適用される絶対的なタイムアウト時刻を設定します。SetReadDeadline(t time.Time)
: 以降の読み込み操作にのみ適用される絶対的なタイムアウト時刻を設定します。SetWriteDeadline(t time.Time)
: 以降の書き込み操作にのみ適用される絶対的なタイムアウト時刻を設定します。 これらのメソッドは、time.Now().Add(duration)
のように、現在時刻に期間を加算して未来の時刻を指定するのが一般的です。
テストのFlakiness(不安定性)
ソフトウェアテストにおける「Flakiness」とは、同じコードに対して同じテストを複数回実行したときに、成功したり失敗したりと結果が不安定になる現象を指します。Flakyテストは、以下のような問題を引き起こします。
- 信頼性の低下: テスト結果が信用できなくなり、開発者がテストの失敗を無視するようになる可能性があります。
- 開発効率の低下: 開発者がテストの失敗が実際のバグによるものか、それともFlakyテストによるものかを判断するために時間を費やすことになります。
- CI/CDパイプラインの阻害: CI/CDパイプラインがFlakyテストによって頻繁に中断され、デプロイメントの速度が低下します。
Flakinessの一般的な原因としては、並行処理の競合状態、外部サービスへの依存、環境のばらつき(ネットワーク遅延、ディスクI/O速度、CPU負荷など)、そして今回のように不適切なタイムアウト設定などが挙げられます。
time.Duration
とtime.Time
Go言語のtime
パッケージは、時間と期間を扱うための強力な機能を提供します。
time.Duration
: 期間を表す型です。例えば、10 * time.Second
は10秒間を表します。time.Time
: 特定の時点を表す型です。time.Now()
は現在の時刻を返します。time.Now().Add(duration)
: 現在の時刻に指定された期間を加算し、未来の時刻をtime.Time
型で返します。
技術的詳細
このコミットの技術的な核心は、テストのタイムアウト設定の変更です。
Goのnet
パッケージのSetDeadline
系のメソッドは、ネットワーク操作が指定された時刻までに完了しない場合にエラーを返すように設計されています。これは、アプリケーションがハングアップするのを防ぎ、リソースを適切に解放するために非常に重要な機能です。
しかし、テストコードにおいて、このタイムアウト機能そのものを厳密にテストするのではなく、単にSetDeadline
メソッドが呼び出されてもプログラムがクラッシュしないこと、または基本的なネットワーク操作がタイムアウトせずに完了することを確認したい場合があります。このような場合、タイムアウト値を非常に短く設定すると、テストが実行される環境のわずかな変動(例えば、OSのスケジューラがテストプロセスにCPU時間を割り当てるのが遅れた、ネットワークスタックが一時的にビジーだった、ガベージコレクションが走ったなど)によって、意図しないタイムアウトが発生し、テストが失敗する可能性があります。
元のコードでは、100 * time.Millisecond
(100ミリ秒)という非常に短いタイムアウトが設定されていました。これは、多くのシステムにおいて、ネットワーク接続の確立や少量のデータの送受信といった基本的な操作が、常に100ミリ秒以内に完了することを保証するには短すぎます。特に、CI環境のような共有リソースや仮想化された環境では、この問題が顕著になります。
このコミットでは、someTimeout
という新しい定数を導入し、その値を10 * time.Second
(10秒)に設定しました。そして、テスト内でSetDeadline
系のメソッドを呼び出す際に、このsomeTimeout
を使用するように変更しました。
// someTimeout is used just to test that net.Conn implementations
// don't explode when their SetFooDeadline methods are called.
// It isn't actually used for testing timeouts.
const someTimeout = 10 * time.Second
この変更により、タイムアウト値が十分に長くなり、通常のネットワーク操作が完了するまでに十分な時間が確保されるようになりました。テストの目的は「タイムアウトが正しく機能するか」ではなく、「SetFooDeadline
メソッドが呼ばれても実装が壊れないか」であるため、この長いタイムアウトはテストの意図に合致し、かつテストの安定性を大幅に向上させます。
これは、テストの設計原則における重要な考慮事項を示しています。すなわち、テストは可能な限り独立しており、外部環境の変動に左右されにくいように設計されるべきです。特定の機能(この場合はタイムアウト)をテストする際には厳密なタイムアウトを設定すべきですが、その機能の副作用(この場合はSetDeadline
の呼び出し)を確認するだけであれば、より寛容な設定を用いるべきです。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのテストファイルが変更されています。
src/pkg/net/conn_test.go
src/pkg/net/protoconn_test.go
変更の具体的な内容は、time.Now().Add(100 * time.Millisecond)
という記述が、新しく定義された定数someTimeout
を使ったtime.Now().Add(someTimeout)
に置き換えられている点です。
src/pkg/net/conn_test.go
の変更
--- a/src/pkg/net/conn_test.go
+++ b/src/pkg/net/conn_test.go
@@ -23,6 +23,11 @@ var connTests = []struct {
{"unixpacket", testUnixAddr()},\n }\n \n+// someTimeout is used just to test that net.Conn implementations
+// don't explode when their SetFooDeadline methods are called.
+// It isn't actually used for testing timeouts.
+const someTimeout = 10 * time.Second
+\n func TestConnAndListener(t *testing.T) {\n \tfor _, tt := range connTests {\n \t\tswitch tt.net {\n@@ -59,9 +64,9 @@ func TestConnAndListener(t *testing.T) {\n \t\tdefer c.Close()\n \t\tc.LocalAddr()\n \t\tc.RemoteAddr()\n-\t\tc.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\t\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\t\tc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\t\tc.SetDeadline(time.Now().Add(someTimeout))\n+\t\tc.SetReadDeadline(time.Now().Add(someTimeout))\n+\t\tc.SetWriteDeadline(time.Now().Add(someTimeout))\n \n \t\tif _, err := c.Write([]byte(\"CONN TEST\")); err != nil {\n \t\t\tt.Fatalf(\"Conn.Write failed: %v\", err)\n@@ -80,9 +85,9 @@ func transponder(t *testing.T, ln Listener, done chan<- int) {\n \n \tswitch ln := ln.(type) {\n \tcase *TCPListener:\n-\t\tln.SetDeadline(time.Now().Add(100 * time.Millisecond))\n+\t\tln.SetDeadline(time.Now().Add(someTimeout))\n \tcase *UnixListener:\n-\t\tln.SetDeadline(time.Now().Add(100 * time.Millisecond))\n+\t\tln.SetDeadline(time.Now().Add(someTimeout))\n \t}\n \tc, err := ln.Accept()\n \tif err != nil {\n@@ -92,9 +97,9 @@ func transponder(t *testing.T, ln Listener, done chan<- int) {\n \tdefer c.Close()\n \tc.LocalAddr()\n \tc.RemoteAddr()\n-\tc.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc.SetDeadline(time.Now().Add(someTimeout))\n+\tc.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc.SetWriteDeadline(time.Now().Add(someTimeout))\n \n \tb := make([]byte, 128)\n \tn, err := c.Read(b)\n```
### `src/pkg/net/protoconn_test.go` の変更
```diff
--- a/src/pkg/net/protoconn_test.go
+++ b/src/pkg/net/protoconn_test.go
@@ -105,9 +105,9 @@ func TestTCPConnSpecificMethods(t *testing.T) {\n \tc.SetNoDelay(false)\n \tc.LocalAddr()\n \tc.RemoteAddr()\n-\tc.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc.SetDeadline(time.Now().Add(someTimeout))\n+\tc.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc.SetWriteDeadline(time.Now().Add(someTimeout))\n \n \tif _, err := c.Write([]byte(\"TCPCONN TEST\")); err != nil {\n \t\tt.Fatalf(\"TCPConn.Write failed: %v\", err)\n@@ -132,9 +132,9 @@ func TestUDPConnSpecificMethods(t *testing.T) {\n \tdefer c.Close()\n \tc.LocalAddr()\n \tc.RemoteAddr()\n-\tc.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc.SetDeadline(time.Now().Add(someTimeout))\n+\tc.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc.SetWriteDeadline(time.Now().Add(someTimeout))\n \tc.SetReadBuffer(2048)\n \tc.SetWriteBuffer(2048)\n \n@@ -180,9 +180,9 @@ func TestIPConnSpecificMethods(t *testing.T) {\n \tdefer c.Close()\n \tc.LocalAddr()\n \tc.RemoteAddr()\n-\tc.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc.SetDeadline(time.Now().Add(someTimeout))\n+\tc.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc.SetWriteDeadline(time.Now().Add(someTimeout))\n \tc.SetReadBuffer(2048)\n \tc.SetWriteBuffer(2048)\n \n@@ -279,9 +279,9 @@ func TestUnixConnSpecificMethods(t *testing.T) {\n \tdefer os.Remove(addr1)\n \tc1.LocalAddr()\n \tc1.RemoteAddr()\n-\tc1.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc1.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc1.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc1.SetDeadline(time.Now().Add(someTimeout))\n+\tc1.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc1.SetWriteDeadline(time.Now().Add(someTimeout))\n \tc1.SetReadBuffer(2048)\n \tc1.SetWriteBuffer(2048)\n \n@@ -297,9 +297,9 @@ func TestUnixConnSpecificMethods(t *testing.T) {\n \tdefer os.Remove(addr2)\n \tc2.LocalAddr()\n \tc2.RemoteAddr()\n-\tc2.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc2.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc2.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc2.SetDeadline(time.Now().Add(someTimeout))\n+\tc2.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc2.SetWriteDeadline(time.Now().Add(someTimeout))\n \tc2.SetReadBuffer(2048)\n \tc2.SetWriteBuffer(2048)\n \n@@ -315,9 +315,9 @@ func TestUnixConnSpecificMethods(t *testing.T) {\n \tdefer os.Remove(addr3)\n \tc3.LocalAddr()\n \tc3.RemoteAddr()\n-\tc3.SetDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc3.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n-\tc3.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))\n+\tc3.SetDeadline(time.Now().Add(someTimeout))\n+\tc3.SetReadDeadline(time.Now().Add(someTimeout))\n+\tc3.SetWriteDeadline(time.Now().Add(someTimeout))\n \tc3.SetReadBuffer(2048)\n \tc3.SetWriteBuffer(2048)\n \n```
## コアとなるコードの解説
変更の核となるのは、`someTimeout`という定数の導入と、それを用いた`SetDeadline`系メソッドの呼び出しです。
```go
// someTimeout is used just to test that net.Conn implementations
// don't explode when their SetFooDeadline methods are called.
// It isn't actually used for testing timeouts.
const someTimeout = 10 * time.Second
このコメントは、この定数の目的を明確に説明しています。「net.Conn
の実装がSetFooDeadline
メソッドが呼ばれたときに壊れないことをテストするためだけに使用される。実際にタイムアウトをテストするために使用されるわけではない。」という点が重要です。
以前のコードでは、time.Now().Add(100 * time.Millisecond)
のように、その場で100ミリ秒のタイムアウトを計算して設定していました。この短い期間は、テストが実行される環境のわずかな遅延によって、テストが失敗する原因となっていました。
新しいコードでは、someTimeout
という10秒の定数を使用することで、テストがネットワーク操作を完了するのに十分な時間を与えています。これにより、テストは「SetDeadline
メソッドが正しく呼び出され、接続オブジェクトがその呼び出しを処理できること」を確認するという本来の目的に集中できるようになり、環境依存の不安定性が排除されました。
この変更は、テストの信頼性を高めるための一般的なプラクティスを示しています。すなわち、テストの目的が特定の機能の厳密なタイミングやパフォーマンスを検証することではない場合、外部要因による不安定性を避けるために、より寛容なタイムアウトや待機時間を使用することが推奨されます。
関連リンク
- Go Issue 4969: https://github.com/golang/go/issues/4969
- Go CL 7456049: https://golang.org/cl/7456049
参考にした情報源リンク
- Go言語の
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語の
time
パッケージのドキュメント: https://pkg.go.dev/time - ソフトウェアテストにおけるFlakinessに関する一般的な情報源 (例: Google検索 "flaky tests", "test flakiness")