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

[インデックス 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 メソッドのタイムアウトが発火してしまうことにありました。元の実装では、SetReadDeadlinestartServer 関数内で一度だけ設定されていました。これにより、runSyslog ゴルーチン内の ReadFrom ループが、最初の読み込み以降、有効期限切れのデッドライン(または非常に短いデッドライン)で動作する可能性がありました。結果として、データが到着する前にタイムアウトエラーが発生し、テストが期待するデータを受信できずに失敗するという「不安定な(flakey)」挙動を引き起こしていました。

このコミットは、テストの信頼性を向上させ、開発者がテスト結果に一貫性を持って依存できるようにするために行われました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とネットワークプログラミングの基礎知識が役立ちます。

  1. Go言語の並行処理 (Goroutines and Channels):

    • Goroutine: Go言語における軽量なスレッドのようなもので、go キーワードを使って関数を並行実行します。このコミットでは、runSyslog 関数が別のゴルーチンとして実行され、メインのテストロジックと並行してネットワークからのデータ受信を試みます。
    • Channel: ゴルーチン間でデータを安全にやり取りするための通信メカニズムです。done chan<- string は、runSyslog ゴルーチンが処理を完了した際に受信したデータを送信するためのチャネルです。
  2. Go言語の net パッケージ:

    • net.PacketConn: UDPなどのパケット指向のネットワーク接続を表すインターフェースです。このテストでは、UDPソケットをリッスンするために使用されます。
    • ReadFrom(b []byte): net.PacketConn インターフェースのメソッドで、ネットワーク接続からパケットを読み込み、読み込んだバイト数、送信元アドレス、およびエラーを返します。
    • SetReadDeadline(t time.Time): ネットワーク接続の読み込み操作にタイムアウトを設定します。指定された時刻 t までにデータが読み込めなかった場合、ReadFrom はエラーを返します。
  3. Go言語の time パッケージ:

    • time.Now(): 現在の時刻を返します。
    • time.Add(d Duration): 指定された期間 d を時刻に加算します。
    • time.Millisecond: 1ミリ秒を表す time.Duration 型の定数です。
    • time.Duration: 時間の長さを表す型です。
  4. Syslog:

    • システムやネットワーク機器からログメッセージを収集するための標準的なプロトコルです。このテストは、Goの log/syslog パッケージがsyslogメッセージを正しく送信・受信できることを検証しています。
  5. 単体テストと不安定なテスト (Flakey Tests):

    • 単体テスト: ソフトウェアの個々のコンポーネントが正しく動作するかを検証するテストです。
    • モックサーバー: 実際の外部サービス(この場合はsyslogサーバー)の動作を模倣するダミーのサーバーです。テストの分離と再現性を高めるために使用されます。
    • 不安定なテスト (Flakey Test): 同じコードに対して、同じ条件で実行しても、成功したり失敗したりするテストのことです。通常、並行処理の競合状態、外部リソースの可用性、またはタイムアウトの不適切な設定などが原因で発生します。このコミットはまさにこの問題に対処しています。

技術的詳細

このコミットの技術的な核心は、net.PacketConn.SetReadDeadline の適用方法と、ReadFrom からの戻り値(特にエラーと読み込みバイト数)の処理順序の変更にあります。

  1. 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ミリ秒のデッドラインが設定されます。これにより、各読み込み操作が独立したタイムアウトを持つようになり、テストがより安定して動作するようになります。データが到着するのを待つ時間が、各読み込み試行で確実に確保されるため、低速なホストでもデータ受信の機会が増えます。
  2. 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 のチェックが不要になりました。なぜなら、ReadFromn=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 関数に集中しています。

  1. 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 のチェックは不要になりました。なぜなら、ReadFromn=0 を返すのは通常エラーを伴う場合であり、その場合は err != nil でループが終了するためです。
  2. startServer 関数内:

    • c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) の行が削除されました。このデッドライン設定は runSyslog 関数内のループに移動されたため、ここでは不要になりました。

これらの変更により、runSyslog ゴルーチンは、各読み込み操作に対して独立したタイムアウトを持ち、かつ、エラーが発生した場合でも部分的に受信したデータを適切に処理するようになり、テストの信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント:
  • Go言語における並行処理とチャネルに関する一般的な情報源。
  • 単体テストとテストの不安定性(flakey tests)に関する一般的なソフトウェアエンジニアリングの概念。