[インデックス 14634] ファイルの概要
このコミットは、Go言語の標準ライブラリ log/syslog
パッケージ内のテスト (syslog_test.go
) における、低速なホスト環境で発生する可能性のある不安定な(flakey)テストの失敗を修正するものです。具体的には、モックのsyslogサーバーがデータを受信する前にタイムアウトが発生してしまう問題を解決し、テストの信頼性を向上させています。
コミット
- コミットハッシュ:
ae12e963505f71bfd5ddb427ba0f0c546c422c30
- 作者: Dave Cheney dave@cheney.net
- コミット日時: 2012年12月13日 木曜日 16:26:20 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ae12e963505f71bfd5ddb427ba0f0c546c422c30
元コミット内容
log/syslog: fix flakey test on slow hosts
Fixes #4467.
The syslog tests can fail if the timeout fires before the data arrives at the mock server. Moving the timeout onto the goroutine that is calling ReadFrom() and always processing the data returned before handling the error should improve the reliability of the test.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6920047
変更の背景
このコミットの背景には、Go言語の log/syslog
パッケージのテストが、特定の環境、特にI/O処理が遅いホストや負荷の高いシステムで不安定に失敗するという問題がありました。これはGitHub Issue #4467として報告されていました。
問題の根本原因は、テスト内で使用されるモックのsyslogサーバーが、クライアントからのデータを受信する前に net.PacketConn.ReadFrom
メソッドのタイムアウトが発火してしまうことにありました。元の実装では、SetReadDeadline
が startServer
関数内で一度だけ設定されていました。これにより、runSyslog
ゴルーチン内の ReadFrom
ループが、最初の読み込み以降、有効期限切れのデッドライン(または非常に短いデッドライン)で動作する可能性がありました。結果として、データが到着する前にタイムアウトエラーが発生し、テストが期待するデータを受信できずに失敗するという「不安定な(flakey)」挙動を引き起こしていました。
このコミットは、テストの信頼性を向上させ、開発者がテスト結果に一貫性を持って依存できるようにするために行われました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とネットワークプログラミングの基礎知識が役立ちます。
-
Go言語の並行処理 (Goroutines and Channels):
- Goroutine: Go言語における軽量なスレッドのようなもので、
go
キーワードを使って関数を並行実行します。このコミットでは、runSyslog
関数が別のゴルーチンとして実行され、メインのテストロジックと並行してネットワークからのデータ受信を試みます。 - Channel: ゴルーチン間でデータを安全にやり取りするための通信メカニズムです。
done chan<- string
は、runSyslog
ゴルーチンが処理を完了した際に受信したデータを送信するためのチャネルです。
- Goroutine: Go言語における軽量なスレッドのようなもので、
-
Go言語の
net
パッケージ:net.PacketConn
: UDPなどのパケット指向のネットワーク接続を表すインターフェースです。このテストでは、UDPソケットをリッスンするために使用されます。ReadFrom(b []byte)
:net.PacketConn
インターフェースのメソッドで、ネットワーク接続からパケットを読み込み、読み込んだバイト数、送信元アドレス、およびエラーを返します。SetReadDeadline(t time.Time)
: ネットワーク接続の読み込み操作にタイムアウトを設定します。指定された時刻t
までにデータが読み込めなかった場合、ReadFrom
はエラーを返します。
-
Go言語の
time
パッケージ:time.Now()
: 現在の時刻を返します。time.Add(d Duration)
: 指定された期間d
を時刻に加算します。time.Millisecond
: 1ミリ秒を表すtime.Duration
型の定数です。time.Duration
: 時間の長さを表す型です。
-
Syslog:
- システムやネットワーク機器からログメッセージを収集するための標準的なプロトコルです。このテストは、Goの
log/syslog
パッケージがsyslogメッセージを正しく送信・受信できることを検証しています。
- システムやネットワーク機器からログメッセージを収集するための標準的なプロトコルです。このテストは、Goの
-
単体テストと不安定なテスト (Flakey Tests):
- 単体テスト: ソフトウェアの個々のコンポーネントが正しく動作するかを検証するテストです。
- モックサーバー: 実際の外部サービス(この場合はsyslogサーバー)の動作を模倣するダミーのサーバーです。テストの分離と再現性を高めるために使用されます。
- 不安定なテスト (Flakey Test): 同じコードに対して、同じ条件で実行しても、成功したり失敗したりするテストのことです。通常、並行処理の競合状態、外部リソースの可用性、またはタイムアウトの不適切な設定などが原因で発生します。このコミットはまさにこの問題に対処しています。
技術的詳細
このコミットの技術的な核心は、net.PacketConn.SetReadDeadline
の適用方法と、ReadFrom
からの戻り値(特にエラーと読み込みバイト数)の処理順序の変更にあります。
-
SetReadDeadline
の移動と再設定:- 変更前:
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
はstartServer
関数内で一度だけ設定されていました。これは、runSyslog
ゴルーチンがReadFrom
を呼び出すたびに、そのデッドラインが古くなるか、既に期限切れになっている可能性がありました。特に、最初のReadFrom
呼び出しがタイムアウトした場合、それ以降の呼び出しはすぐにタイムアウトエラーを返すことになり、後続のデータを受信する機会を失っていました。 - 変更後:
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
がrunSyslog
関数内のfor
ループの各イテレーションの開始時に移動されました。これにより、ReadFrom
が呼び出される直前に常に新しい100ミリ秒のデッドラインが設定されます。これにより、各読み込み操作が独立したタイムアウトを持つようになり、テストがより安定して動作するようになります。データが到着するのを待つ時間が、各読み込み試行で確実に確保されるため、低速なホストでもデータ受信の機会が増えます。
- 変更前:
-
ReadFrom
の戻り値の処理順序の変更:- 変更前:
このロジックでは、n, _, err := c.ReadFrom(buf[0:]) if err != nil || n == 0 { break } rcvd += string(buf[0:n])
ReadFrom
がエラーを返した場合(例えばタイムアウトエラー)、または読み込んだバイト数n
が0だった場合に、すぐにループを抜けていました。しかし、ReadFrom
は、一部のデータを読み込んだ後でエラーを返す(例えば、バッファがいっぱいになった後にio.EOF
を返す)という挙動をすることがあります。また、タイムアウトエラーが発生した場合でも、n
が0でない(つまり、一部のデータが読み込まれた)可能性があります。この変更前のロジックでは、n > 0
であってもerr != nil
の場合にそのn
バイトのデータがrcvd
に追加されずに破棄されてしまう可能性がありました。 - 変更後:
この変更により、n, _, err := c.ReadFrom(buf[:]) rcvd += string(buf[:n]) // 常に読み込んだデータを処理 if err != nil { // その後でエラーをチェック break }
ReadFrom
が返したn
バイトのデータは、エラーの有無にかかわらず、常にrcvd
変数に追加されるようになりました。これは、ReadFrom
が部分的な読み込みとエラーを同時に返す可能性があるというGoのI/Oインターフェースの特性を考慮したものです。これにより、タイムアウトが発生した場合でも、それまでに受信したデータが失われることなく処理されることが保証され、テストの堅牢性が向上します。また、n == 0
のチェックが不要になりました。なぜなら、ReadFrom
がn=0
を返すのは通常、エラーが発生した場合であり、その場合はerr != nil
でループを抜けるためです。
- 変更前:
これらの変更により、テストはネットワークの遅延やシステム負荷の影響を受けにくくなり、より信頼性の高い結果を提供するようになりました。
コアとなるコードの変更箇所
src/pkg/log/syslog/syslog_test.go
ファイルが変更されました。
--- a/src/pkg/log/syslog/syslog_test.go
+++ b/src/pkg/log/syslog/syslog_test.go
@@ -20,13 +20,14 @@ var serverAddr string
func runSyslog(c net.PacketConn, done chan<- string) {
var buf [4096]byte
- var rcvd string = ""
+ var rcvd string
for {
- n, _, err := c.ReadFrom(buf[0:])
- if err != nil || n == 0 {
+ c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
+ n, _, err := c.ReadFrom(buf[:])
+ rcvd += string(buf[:n])
+ if err != nil {
break
}
- rcvd += string(buf[0:n])
}
done <- rcvd
}
@@ -37,7 +38,6 @@ func startServer(done chan<- string) {
log.Fatalf("net.ListenPacket failed udp :0 %v", e)
}\n serverAddr = c.LocalAddr().String()\n-\tc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n \tgo runSyslog(c, done)\n }\n
コアとなるコードの解説
変更は主に runSyslog
関数と startServer
関数に集中しています。
-
runSyslog
関数内:var rcvd string = ""
がvar rcvd string
に変更されました。これは初期化の冗長性をなくすための小さな変更で、Goでは文字列型のゼロ値は空文字列なので、実質的な動作変更はありません。c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
の行が、for
ループの先頭、c.ReadFrom
の直前に移動されました。これにより、ReadFrom
が呼び出されるたびに、常に新しい100ミリ秒の読み込みデッドラインが設定されるようになります。n, _, err := c.ReadFrom(buf[0:])
がn, _, err := c.ReadFrom(buf[:])
に変更されました。これはスライス表記の簡略化であり、機能的な違いはありません。rcvd += string(buf[:n])
の行が、if err != nil
のチェックの前に移動されました。これにより、ReadFrom
がエラーを返した場合でも、それまでに読み込まれた有効なバイト数n
のデータがrcvd
に確実に連結されるようになります。if err != nil || n == 0 { break }
がif err != nil { break }
に変更されました。前述の通り、n == 0
のチェックは不要になりました。なぜなら、ReadFrom
がn=0
を返すのは通常エラーを伴う場合であり、その場合はerr != nil
でループが終了するためです。
-
startServer
関数内:c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
の行が削除されました。このデッドライン設定はrunSyslog
関数内のループに移動されたため、ここでは不要になりました。
これらの変更により、runSyslog
ゴルーチンは、各読み込み操作に対して独立したタイムアウトを持ち、かつ、エラーが発生した場合でも部分的に受信したデータを適切に処理するようになり、テストの信頼性が大幅に向上しました。
関連リンク
- GitHub Issue: https://github.com/golang/go/issues/4467
- Go Code Review (CL): https://golang.org/cl/6920047
参考にした情報源リンク
- Go言語公式ドキュメント:
net
パッケージ: https://pkg.go.dev/nettime
パッケージ: https://pkg.go.dev/timenet.PacketConn
インターフェース: https://pkg.go.dev/net#PacketConnnet.PacketConn.ReadFrom
メソッドの挙動に関する一般的な情報 (GoのI/Oインターフェースの設計原則): https://pkg.go.dev/io#Reader (特に、Read
メソッドが(n int, err error)
を返す際のn > 0
かつerr != nil
のケースについて)
- Go言語における並行処理とチャネルに関する一般的な情報源。
- 単体テストとテストの不安定性(flakey tests)に関する一般的なソフトウェアエンジニアリングの概念。