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

[インデックス 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.Durationtime.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つのテストファイルが変更されています。

  1. src/pkg/net/conn_test.go
  2. 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言語のnetパッケージのドキュメント: https://pkg.go.dev/net
  • Go言語のtimeパッケージのドキュメント: https://pkg.go.dev/time
  • ソフトウェアテストにおけるFlakinessに関する一般的な情報源 (例: Google検索 "flaky tests", "test flakiness")