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

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

このコミットは、Go言語の標準ライブラリである log/syslog パッケージのテストファイル src/pkg/log/syslog/syslog_test.go における競合状態(race condition)を修正するものです。具体的には、テスト中にチャネルのクローズとネットワークリスナーの Accept メソッドの呼び出しとの間で発生する競合を解消し、テストの信頼性と安定性を向上させています。

コミット

commit 67d0445c876e2015db0cf39dd26ed5643dc77ac3
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Feb 28 07:48:16 2013 +0100

    log/syslog: fix race in test between channel close and accept.
    
    Fixes #4769.
    
    R=golang-dev, dave, adg, bradfitz
    CC=fullung, golang-dev
    https://golang.org/cl/7322089

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

https://github.com/golang/go/commit/67d0445c876e2015db0cf39dd26ed5643dc77ac3

元コミット内容

このコミットは、log/syslog パッケージのテストコードにおいて、チャネルのクローズ処理とネットワーク接続の Accept 処理との間で発生する競合状態を修正することを目的としています。この競合状態は、テストが不安定になったり、ハングアップしたりする原因となっていました。コミットメッセージには、この修正がIssue #4769を解決することが明記されています。

変更の背景

Go言語の log/syslog パッケージは、システムログメッセージをSyslogサーバーに送信するための機能を提供します。このパッケージのテストでは、Syslogサーバーをシミュレートするために、ネットワークリスナー(net.Listener)を起動し、クライアントからの接続を受け入れる処理が含まれています。

問題となっていたのは、テストの終了時や再起動時に、リスナーがクローズされるタイミングと、リスナーが新しい接続を Accept しようとしているタイミングが重なることで発生する競合状態でした。具体的には、net.Listener.Accept() はブロックする操作であり、リスナーがクローズされるとエラーを返してブロックが解除されます。しかし、テストコード内でこのエラーが適切に処理されなかったり、関連するゴルーチン(runStreamSyslog)が正常に終了しなかったりすると、テストがデッドロックに陥ったり、予期せぬエラーで失敗したりする可能性がありました。

Issue #4769では、このテストの不安定性が報告されており、特に TestFlapTestConcurrentReconnect のような、サーバーの起動と停止を繰り返すテストで顕著でした。このコミットは、リスナーのライフサイクル管理を改善し、ゴルーチンの同期を強化することで、この競合状態を根本的に解決することを目指しています。

前提知識の解説

Goの並行処理

  • Goroutine (ゴルーチン): Go言語における軽量な実行スレッドです。go キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、並行して実行されます。
  • Channel (チャネル): ゴルーチン間でデータを安全に送受信するための通信メカニズムです。チャネルを通じてデータをやり取りすることで、共有メモリによる競合状態を避けることができます。
  • sync.WaitGroup: 複数のゴルーチンの完了を待つための同期プリミティブです。Add(n) で待つゴルーチンの数を設定し、各ゴルーチンが完了するたびに Done() を呼び出し、すべてのゴルーチンが完了するまで Wait() で待機します。

net.Listener.Accept() の挙動

net.Listener インターフェースの Accept() メソッドは、新しいネットワーク接続を待ち受け、接続が確立されると net.Conn インターフェースを実装するオブジェクトとエラーを返します。接続がない場合はブロックし、リスナーがクローズされるとエラー(通常は net.OpErroruse of closed network connection のようなメッセージ)を返してブロックが解除されます。

レースコンディション(競合状態)

複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスし、そのアクセス順序によって結果が非決定的に変わる状態を指します。テスト環境では、このような非決定性がテストの不安定性(特定の条件下でのみ失敗する、再現性が低いなど)を引き起こし、バグの特定や修正を困難にします。

log/syslog パッケージ

Goの標準ライブラリ log/syslog は、RFC 3164 (BSD Syslog Protocol) および RFC 5424 (Syslog Protocol) に基づいて、Syslogメッセージを生成し、ネットワーク経由でSyslogサーバーに送信するための機能を提供します。

io.Closer インターフェース

io.Closer は、Close() error メソッドを持つインターフェースです。ファイル、ネットワーク接続、その他のリソースなど、使用後に明示的にクリーンアップが必要なオブジェクトによく実装されます。defer ステートメントと組み合わせて使用することで、関数の終了時にリソースが確実にクローズされるようにすることができます。

技術的詳細

このコミットの主要な変更点は、テストヘルパー関数 startServer のシグネチャ変更と、それに伴うリスナーのクローズ処理の明示化、およびゴルーチンの同期の改善です。

  1. startServer 関数のシグネチャ変更:

    • 変更前: func startServer(n, la string, done chan<- string) (addr string, wg *sync.WaitGroup)
    • 変更後: func startServer(n, la string, done chan<- string) (addr string, sock io.Closer, wg *sync.WaitGroup)
    • 新しい戻り値 sock io.Closer が追加されました。これにより、startServer が作成した net.Listener オブジェクト(io.Closer インターフェースを実装している)を呼び出し元に返し、呼び出し元が明示的にリスナーをクローズできるようになります。これは、テストのクリーンアップフェーズで非常に重要です。
  2. runStreamSyslog ゴルーチンの WaitGroup 管理の改善:

    • 以前は go runStreamSyslog(l, done, wg) のように直接ゴルーチンを起動していましたが、変更後は匿名関数でラップし、wg.Add(1)defer wg.Done() を追加しました。
    • これにより、runStreamSyslog ゴルーチンが開始される前に wg.Add(1) が確実に呼び出され、ゴルーチンが終了する際に defer wg.Done() が確実に呼び出されるようになります。これにより、WaitGroup がゴルーチンのライフサイクルを正確に追跡し、テストがゴルーチンの完了を正しく待機できるようになります。
  3. テスト関数での defer sock.Close() の導入:

    • TestFlap, TestWrite, TestConcurrentWrite などのテスト関数で、startServer から返された sock オブジェクトに対して defer sock.Close() が追加されました。
    • これにより、テスト関数が終了する際に、起動したネットワークリスナーが確実にクローズされ、リソースリークを防ぎ、次のテスト実行への影響を排除します。特に、TestFlap のようにサーバーを再起動するシナリオでは、古いリスナーが適切にクローズされることが不可欠です。
  4. TestConcurrentReconnect における sock.Close() のタイミング:

    • このテストでは、複数のクライアントが同時に再接続を試みるシナリオをシミュレートします。以前は、クライアント側の wg.Wait() の後にサーバー側の srvWG.Wait() を呼び出していましたが、sock.Close() が追加されました。
    • wg.Wait() (クライアントゴルーチンの完了待機) の後に sock.Close() (サーバーリスナーのクローズ) を行い、その後に srvWG.Wait() (サーバーゴルーチンの完了待機) を行うことで、リスナーがクローズされた後に runStreamSyslog ゴルーチンが Accept からエラーを返して正常に終了するのを待つという、より安全なシャットダウンシーケンスが確立されます。これにより、Accept がブロックされたままテストが終了する競合状態が解消されます。

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

src/pkg/log/syslog/syslog_test.go の以下の部分がコアとなる変更箇所です。

--- a/src/pkg/log/syslog/syslog_test.go
+++ b/src/pkg/log/syslog/syslog_test.go
@@ -51,7 +51,6 @@ func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) {
 		var c net.Conn
 		var err error
 		if c, err = l.Accept(); err != nil {
-			fmt.Print(err)
 			return
 		}
 		wg.Add(1)
@@ -71,7 +70,7 @@ func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) {
 	}
 }
 
-func startServer(n, la string, done chan<- string) (addr string, wg *sync.WaitGroup) {
+func startServer(n, la string, done chan<- string) (addr string, sock io.Closer, wg *sync.WaitGroup) {
 	if n == "udp" || n == "tcp" {
 		la = "127.0.0.1:0"
 	} else {
@@ -95,6 +94,7 @@ func startServer(n, la string, done chan<- string) (addr string, wg *sync.WaitGr
 			log.Fatalf("startServer failed: %v", e)
 		}
 		addr = l.LocalAddr().String()
+		sock = l
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
@@ -106,7 +106,12 @@ func startServer(n, la string, done chan<- string) (addr string, wg *sync.WaitGr
 			log.Fatalf("startServer failed: %v", e)
 		}
 		addr = l.Addr().String()
-		go runStreamSyslog(l, done, wg)
+		sock = l
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			runStreamSyslog(l, done, wg)
+		}()
 	}
 	return
 }
@@ -117,7 +122,7 @@ func TestWithSimulated(t *testing.T) {
 
 	for _, tr := range transport {
 		done := make(chan string)
-		addr, _ := startServer(tr, "", done)
+		addr, _, _ := startServer(tr, "", done)
 		if tr == "unix" || tr == "unixgram" {
 			defer os.Remove(addr)
 		}
@@ -137,8 +142,9 @@ func TestFlap(t *testing.T) {
 func TestFlap(t *testing.T) {
 	net := "unix"
 	done := make(chan string)
-	addr, _ := startServer(net, "", done)
+	addr, sock, _ := startServer(net, "", done)
 	defer os.Remove(addr)
+	defer sock.Close()
 
 	s, err := Dial(net, addr, LOG_INFO|LOG_USER, "syslog_test")
 	if err != nil {
@@ -152,7 +158,8 @@ func TestFlap(t *testing.T) {\n 	check(t, msg, <-done)\n \n 	// restart the server
-\tstartServer(net, addr, done)\n+\t_, sock2, _ := startServer(net, addr, done)\n+\tdefer sock2.Close()\n \n 	// and try retransmitting
 	msg = "Moo 3"
@@ -242,7 +249,8 @@ func TestWrite(t *testing.T) {
 	} else {
 		for _, test := range tests {
 			done := make(chan string)
-			addr, _ := startServer("udp", "", done)
+			addr, sock, _ := startServer("udp", "", done)
+			defer sock.Close()
 			l, err := Dial("udp", addr, test.pri, test.pre)
 			if err != nil {
 				t.Fatalf("syslog.Dial() failed: %v", err)
@@ -263,7 +271,8 @@ func TestWrite(t *testing.T) {
 }\n \n func TestConcurrentWrite(t *testing.T) {
-\taddr, _ := startServer("udp", "", make(chan string))\n+\taddr, sock, _ := startServer("udp", "", make(chan string))\n+\tdefer sock.Close()\n \tw, err := Dial("udp", addr, LOG_USER|LOG_ERR, "how's it going?")
 \tif err != nil {
 \t\tt.Fatalf("syslog.Dial() failed: %v", err)
@@ -289,7 +298,7 @@ func TestConcurrentReconnect(t *testing.T) {
 
 	net := "unix"
 	done := make(chan string)
-\taddr, srvWG := startServer(net, "", done)\n+\taddr, sock, srvWG := startServer(net, "", done)\n \tdefer os.Remove(addr)\n 
 \t// count all the messages arriving
@@ -327,6 +336,7 @@ func TestConcurrentReconnect(t *testing.T) {
 		}()
 	}
 	wg.Wait()
+\tsock.Close()
 	srvWG.Wait()
 	close(done)
 

コアとなるコードの解説

このコミットの核心は、テストのライフサイクルにおけるリソース(特にネットワークリスナー)の管理を厳密にすることで、並行処理における競合状態を排除することにあります。

  1. startServer の戻り値に io.Closer を追加:

    • 以前は startServer が起動したリスナーオブジェクトを直接返すことはありませんでした。そのため、テストコード側でリスナーを明示的にクローズする手段がありませんでした。
    • sock io.Closer を戻り値に追加することで、startServer が作成した net.Listener インスタンスが呼び出し元に渡されます。これにより、テスト関数は defer sock.Close() を使用して、テストが終了する際にリスナーが確実にクローズされるように設定できます。リスナーがクローズされると、Accept() はブロックを解除し、エラーを返してゴルーチンが正常に終了できるようになります。
  2. runStreamSyslog ゴルーチンの WaitGroup 管理の改善:

    • go func() { defer wg.Done(); runStreamSyslog(l, done, wg) }() という形式に変更されたことで、runStreamSyslog を実行するゴルーチンが wg.Add(1)WaitGroup に登録され、そのゴルーチンが終了する際に defer wg.Done() によって WaitGroup から確実に除外されるようになりました。
    • これにより、startServer が起動するサーバーゴルーチンが WaitGroup によって正確に追跡され、テストが srvWG.Wait() を呼び出したときに、すべてのサーバーゴルーチンが実際に終了するまで待機できるようになります。これは、テストのクリーンアップフェーズで、すべてのバックグラウンド処理が完了していることを保証するために不可欠です。
  3. TestConcurrentReconnect における sock.Close() の戦略的な配置:

    • このテストでは、クライアントがメッセージを送信し終えた後 (wg.Wait())、サーバーリスナーを明示的にクローズ (sock.Close()) しています。これにより、runStreamSyslog ゴルーチンが Accept() からエラーを受け取り、速やかに終了できるようになります。
    • その後、srvWG.Wait() を呼び出すことで、サーバーゴルーチンが完全に終了するのを待機します。この順序付けにより、リスナーがクローズされた後に Accept がブロックされたままになるという競合状態が解消され、テストがデッドロックすることなく確実に終了するようになります。

これらの変更は、テストコードにおけるリソースのライフサイクル管理と並行処理の同期を強化し、テストの信頼性と再現性を大幅に向上させました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント:
  • Go言語における並行処理と競合状態に関する一般的な情報源。
  • Syslogプロトコルに関するRFCドキュメント (RFC 3164, RFC 5424)。