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

[インデックス 15259] ファイルの概要

このコミットは、Go言語の標準ライブラリである log/syslog パッケージのテストファイル src/pkg/log/syslog/syslog_test.go に加えられた変更を扱っています。このファイルは、log/syslog パッケージが提供するSyslogクライアント機能が正しく動作するかどうかを検証するためのテストスイートを含んでいます。具体的には、Syslogメッセージの送受信、接続の確立と切断、エラーハンドリングなど、様々なシナリオをシミュレートしてテストしています。

コミット

log/syslog: fix channel race in test.

R=golang-dev, minux.ma, iant, bradfitz, dave
CC=golang-dev
https://golang.org/cl/7314057

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/7c5bd322d53cdcbfb59db334ba243502ae170803

元コミット内容

commit 7c5bd322d53cdcbfb59db334ba243502ae170803
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Feb 15 11:07:31 2013 +1100

    log/syslog: fix channel race in test.
    
    R=golang-dev, minux.ma, iant, bradfitz, dave
    CC=golang-dev
    https://golang.org/cl/7314057
---\n src/pkg/log/syslog/syslog_test.go | 27 ++++++++++++++++++---------\n 1 file changed, 18 insertions(+), 9 deletions(-)\n

変更の背景

このコミットの背景には、log/syslog パッケージのテストにおける並行処理の競合状態(race condition)が存在していました。テストコードは、Syslogサーバーをシミュレートするゴルーチンを起動し、クライアント(テスト対象のlog/syslogパッケージ)からのメッセージを受信します。しかし、これらのサーバー側のゴルーチンがテスト関数が終了する前に適切に終了しない場合がありました。

具体的には、テスト関数が done チャネルをクローズして終了しようとする際に、まだ動作中のサーバー側ゴルーチンがそのクローズされたチャネルに書き込もうとしたり、テストが終了した後にリソース(例えば、一時ファイルやネットワーク接続)にアクセスしようとしたりすることで、テストが不安定になったり、ランダムに失敗したりする問題が発生していました。これは、テストの信頼性を損なう重大な問題です。

このコミットは、sync.WaitGroup を導入することで、テスト関数がすべてのサーバー側ゴルーチンの完了を確実に待機するようにし、この競合状態を解消することを目的としています。また、ネットワーク接続の読み取りにタイムアウトを設定することで、テストがハングアップする可能性も低減しています。

前提知識の解説

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

  • Go言語の並行処理:
    • Goroutine(ゴルーチン): Go言語における軽量なスレッドのようなもので、go キーワードを使って関数を並行実行します。
    • Channel(チャネル): ゴルーチン間でデータを安全に送受信するための通信メカニズムです。チャネルを介した通信は、Goの並行処理における主要な同期プリミティブです。
    • sync.WaitGroup: 複数のゴルーチンの完了を待機するための同期プリミティブです。カウンタを持ち、Add でカウンタを増やし、Done で減らし、Wait でカウンタがゼロになるまでブロックします。テストにおいて、バックグラウンドで起動したゴルーチンがすべて終了したことを確認するのに非常に有用です。
    • defer: 関数がリターンする直前に実行されるステートメントをスケジュールします。リソースの解放(ファイルのクローズ、ロックの解除など)によく使われます。
  • log/syslog パッケージ: Unix系システムで標準的に使用されるログプロトコルであるSyslogを扱うためのGo言語の標準ライブラリパッケージです。Syslogメッセージの送信(クライアント側)機能を提供します。
  • Goのテスト: Goのテストフレームワークは、go test コマンドで実行され、_test.go で終わるファイルにテストコードを記述します。テスト関数は Test で始まり、*testing.T 型の引数を取ります。
  • ネットワークプログラミングの基礎:
    • net パッケージ: Go言語でネットワーク通信を行うための基本的な機能を提供します。
    • net.Listener: TCPなどのストリーム指向プロトコルで接続をリッスンするためのインターフェースです。
    • net.Conn: ネットワーク接続を表すインターフェースです。データの読み書きや接続のクローズなどを行います。
    • net.PacketConn: UDPなどのパケット指向プロトコルでパケットを送受信するためのインターフェースです。
    • net.Dial / net.Listen / net.ListenPacket: ネットワーク接続を確立したり、リッスンしたりするための関数です。
    • c.SetReadDeadline(time.Time): ネットワーク接続からの読み取り操作にタイムアウトを設定します。指定された時刻までにデータが読み取れない場合、読み取り操作はエラーを返します。これにより、無限にブロックされることを防ぎます。
  • bufio.Reader: バッファリングされたI/O操作を提供し、効率的な読み取りを可能にします。ReadString('\n') は、指定されたデリミタ(ここでは改行)まで文字列を読み取ります。

技術的詳細

このコミットの主要な技術的変更は、テスト内の並行処理の同期を改善することにあります。

  1. sync.WaitGroup の導入:

    • startServer 関数は、Syslogサーバーをシミュレートするゴルーチンを起動します。この関数は、新しく *sync.WaitGroup 型の値を返すように変更されました。
    • startServer 内で、net.ListenPacket (UDP/Unixgram) または net.Listen (TCP/Unix) によって起動されるサーバーゴルーチン(runPktSyslog または runStreamSyslog)の直前に wg.Add(1) が呼び出されます。これにより、起動されるゴルーチンの数を WaitGroup に登録します。
    • これらのサーバーゴルーチン(またはそれらがさらに起動するゴルーチン)の defer wg.Done() が追加されました。これにより、ゴルーチンが終了する際に WaitGroup のカウンタが減算されます。
    • TestConcurrentReconnect のようなテスト関数では、startServer から返された srvWG に対して srvWG.Wait() が呼び出されます。これにより、テスト関数は、すべてのサーバー側ゴルーチンが wg.Done() を呼び出して終了するまでブロックされます。この同期メカニズムにより、テスト関数が done チャネルをクローズしたり、他のクリーンアップ処理を行ったりする前に、すべてのバックグラウンド処理が完了することが保証されます。
  2. c.SetReadDeadline の追加:

    • runStreamSyslog 関数内の、各クライアント接続を処理するゴルーチンに c.SetReadDeadline(time.Now().Add(5 * time.Second)) が追加されました。
    • これは、クライアントからのデータ読み取り操作(b.ReadString('\n'))が最大5秒間だけブロックされるように設定します。もし5秒以内にデータが到着しない場合、読み取り操作はタイムアウトエラーを返します。
    • この変更は、テスト中にクライアントが予期せず切断されたり、メッセージの送信を停止したりした場合に、サーバー側のゴルーチンが無限にブロックされ続けることを防ぎます。これにより、テストがハングアップする可能性が低減し、より堅牢になります。

これらの変更により、テストの実行がより予測可能になり、競合状態による不安定な失敗が解消されます。

コアとなるコードの変更箇所

src/pkg/log/syslog/syslog_test.go ファイルにおける主要な変更箇所は以下の通りです。

  1. runStreamSyslog 関数のシグネチャ変更と sync.WaitGroup の追加:

    --- a/src/pkg/log/syslog/syslog_test.go
    +++ b/src/pkg/log/syslog/syslog_test.go
    @@ -46,7 +46,7 @@ func runPktSyslog(c net.PacketConn, done chan<- string) {
     
     var crashy = false
     
    -func runStreamSyslog(l net.Listener, done chan<- string) {
    +func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) {
     	for {
      		var c net.Conn
      		var err error
    @@ -54,7 +54,10 @@ func runStreamSyslog(l net.Listener, done chan<- string) {
      		\tfmt.Print(err)
      		\treturn
      		}\n+\t\twg.Add(1)\n \t\tgo func(c net.Conn) {\n+\t\t\tdefer wg.Done()\n+\t\t\tc.SetReadDeadline(time.Now().Add(5 * time.Second))\n \t\t\tb := bufio.NewReader(c)\
    
  2. startServer 関数のシグネチャ変更と sync.WaitGroup の導入:

    --- a/src/pkg/log/syslog/syslog_test.go
    +++ b/src/pkg/log/syslog/syslog_test.go
    @@ -68,7 +71,7 @@ func runStreamSyslog(c net.Conn) {
     	}\n \t}\n }\n \n-func startServer(n, la string, done chan<- string) (addr string) {
    +func startServer(n, la string, done chan<- string) (addr string, wg *sync.WaitGroup) {
     	if n == "udp" || n == "tcp" {
      		la = "127.0.0.1:0"
      	} else {
    @@ -85,20 +88,25 @@ func startServer(n, la string, done chan<- string) (addr string) {
      		os.Remove(la)
      	}\n \n+\twg = new(sync.WaitGroup)\n \tif n == "udp" || n == "unixgram" {
      		l, e := net.ListenPacket(n, la)
      		if e != nil {
      			log.Fatalf("startServer failed: %v", e)
      		}\n \t\taddr = l.LocalAddr().String()\n-\t\tgo runPktSyslog(l, done)\n+\t\twg.Add(1)\n+\t\tgo func() {\n+\t\t\tdefer wg.Done()\n+\t\t\trunPktSyslog(l, done)\n+\t\t}()\n \t} else {\n      		l, e := net.Listen(n, la)
      		if e != nil {
      			log.Fatalf("startServer failed: %v", e)
      		}\n \t\taddr = l.Addr().String()\n-\t\tgo runStreamSyslog(l, done)\n+\t\tgo runStreamSyslog(l, done, wg)\n \t}\n \treturn\
    
  3. テスト関数での startServer の呼び出し箇所の更新: TestWithSimulated, TestFlap, TestWrite, TestConcurrentWrite 関数内で、startServer の戻り値が addr, _ のように変更され、新しい wg の戻り値が適切に処理されています。

  4. TestConcurrentReconnect での srvWG.Wait() の追加:

    --- a/src/pkg/log/syslog/syslog_test.go
    +++ b/src/pkg/log/syslog/syslog_test.go
    @@ -319,6 +327,7 @@ func TestConcurrentReconnect(t *testing.T) {\n \t\t}()\n \t}\n \twg.Wait()\n+\tsrvWG.Wait()\n \tclose(done)\n \n \tselect {\
    

コアとなるコードの解説

  • runStreamSyslog の変更:

    • wg *sync.WaitGroup 引数が追加されたことで、この関数が起動するゴルーチン(各クライアント接続を処理するもの)を WaitGroup で管理できるようになりました。
    • wg.Add(1) は、新しいクライアント接続処理ゴルーチンが起動される直前に呼び出され、WaitGroup のカウンタをインクリメントします。
    • defer wg.Done() は、そのクライアント接続処理ゴルーチンが終了する際に WaitGroup のカウンタをデクリメントするように設定されます。これにより、すべてのクライアント接続処理が完了したことを WaitGroup が追跡できるようになります。
    • c.SetReadDeadline(time.Now().Add(5 * time.Second)) は、クライアントからのデータ読み取りが5秒でタイムアウトするように設定します。これにより、テスト中にクライアントがメッセージを送信しなくなった場合でも、サーバー側のゴルーチンが無限にブロックされることなく、適切に終了できるようになります。これは、テストのハングアップを防ぐ上で非常に重要です。
  • startServer の変更:

    • 戻り値に wg *sync.WaitGroup が追加されました。これにより、この関数が起動するサーバーゴルーチン群を管理するための WaitGroup を呼び出し元に渡せるようになります。
    • wg = new(sync.WaitGroup) で新しい WaitGroup インスタンスが作成されます。
    • UDP/Unixgram のケースでは、runPktSyslog を起動するゴルーチンの外側に wg.Add(1) を置き、そのゴルーチン内で defer wg.Done() を呼び出すように変更されました。これは、runPktSyslog 自体が無限ループで動作するため、そのゴルーチンが起動されたことを WaitGroup に通知し、テスト終了時にそのゴルーチンが終了するのを待つためです。
    • TCP/Unix のケースでは、runStreamSyslog に新しく追加された wg 引数を渡すように変更されました。これにより、runStreamSyslog 内で起動される各クライアント接続処理ゴルーチンが WaitGroup によって管理されるようになります。
  • TestConcurrentReconnect での srvWG.Wait():

    • このテストは、Syslogサーバーへの再接続を伴う並行書き込みをテストします。以前は、クライアント側のゴルーチンがすべて終了するのを wg.Wait() で待っていましたが、サーバー側のゴルーチンがまだ動作している可能性がありました。
    • srvWG.Wait() が追加されたことで、テスト関数は、startServer によって起動されたすべてのサーバー側ゴルーチン(およびそれらが処理するクライアント接続ゴルーチン)が完全に終了するまで待機するようになりました。これにより、テストが done チャネルをクローズする前に、すべてのバックグラウンド処理が完了し、競合状態が解消されます。

これらの変更は、Goの並行処理における「構造化された並行性(structured concurrency)」の原則に従い、ゴルーチンのライフサイクルを適切に管理することで、テストの信頼性と安定性を大幅に向上させています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Go言語における並行処理とテストに関する一般的な知識
  • sync.WaitGroup の使用パターンに関する一般的なプラクティス
  • ネットワークプログラミングにおけるタイムアウト処理の重要性