[インデックス 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では、このテストの不安定性が報告されており、特に TestFlap
や TestConcurrentReconnect
のような、サーバーの起動と停止を繰り返すテストで顕著でした。このコミットは、リスナーのライフサイクル管理を改善し、ゴルーチンの同期を強化することで、この競合状態を根本的に解決することを目指しています。
前提知識の解説
Goの並行処理
- Goroutine (ゴルーチン): Go言語における軽量な実行スレッドです。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、並行して実行されます。 - Channel (チャネル): ゴルーチン間でデータを安全に送受信するための通信メカニズムです。チャネルを通じてデータをやり取りすることで、共有メモリによる競合状態を避けることができます。
- sync.WaitGroup: 複数のゴルーチンの完了を待つための同期プリミティブです。
Add(n)
で待つゴルーチンの数を設定し、各ゴルーチンが完了するたびにDone()
を呼び出し、すべてのゴルーチンが完了するまでWait()
で待機します。
net.Listener.Accept()
の挙動
net.Listener
インターフェースの Accept()
メソッドは、新しいネットワーク接続を待ち受け、接続が確立されると net.Conn
インターフェースを実装するオブジェクトとエラーを返します。接続がない場合はブロックし、リスナーがクローズされるとエラー(通常は net.OpError
で use 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
のシグネチャ変更と、それに伴うリスナーのクローズ処理の明示化、およびゴルーチンの同期の改善です。
-
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
インターフェースを実装している)を呼び出し元に返し、呼び出し元が明示的にリスナーをクローズできるようになります。これは、テストのクリーンアップフェーズで非常に重要です。
- 変更前:
-
runStreamSyslog
ゴルーチンのWaitGroup
管理の改善:- 以前は
go runStreamSyslog(l, done, wg)
のように直接ゴルーチンを起動していましたが、変更後は匿名関数でラップし、wg.Add(1)
とdefer wg.Done()
を追加しました。 - これにより、
runStreamSyslog
ゴルーチンが開始される前にwg.Add(1)
が確実に呼び出され、ゴルーチンが終了する際にdefer wg.Done()
が確実に呼び出されるようになります。これにより、WaitGroup
がゴルーチンのライフサイクルを正確に追跡し、テストがゴルーチンの完了を正しく待機できるようになります。
- 以前は
-
テスト関数での
defer sock.Close()
の導入:TestFlap
,TestWrite
,TestConcurrentWrite
などのテスト関数で、startServer
から返されたsock
オブジェクトに対してdefer sock.Close()
が追加されました。- これにより、テスト関数が終了する際に、起動したネットワークリスナーが確実にクローズされ、リソースリークを防ぎ、次のテスト実行への影響を排除します。特に、
TestFlap
のようにサーバーを再起動するシナリオでは、古いリスナーが適切にクローズされることが不可欠です。
-
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)
コアとなるコードの解説
このコミットの核心は、テストのライフサイクルにおけるリソース(特にネットワークリスナー)の管理を厳密にすることで、並行処理における競合状態を排除することにあります。
-
startServer
の戻り値にio.Closer
を追加:- 以前は
startServer
が起動したリスナーオブジェクトを直接返すことはありませんでした。そのため、テストコード側でリスナーを明示的にクローズする手段がありませんでした。 sock io.Closer
を戻り値に追加することで、startServer
が作成したnet.Listener
インスタンスが呼び出し元に渡されます。これにより、テスト関数はdefer sock.Close()
を使用して、テストが終了する際にリスナーが確実にクローズされるように設定できます。リスナーがクローズされると、Accept()
はブロックを解除し、エラーを返してゴルーチンが正常に終了できるようになります。
- 以前は
-
runStreamSyslog
ゴルーチンのWaitGroup
管理の改善:go func() { defer wg.Done(); runStreamSyslog(l, done, wg) }()
という形式に変更されたことで、runStreamSyslog
を実行するゴルーチンがwg.Add(1)
でWaitGroup
に登録され、そのゴルーチンが終了する際にdefer wg.Done()
によってWaitGroup
から確実に除外されるようになりました。- これにより、
startServer
が起動するサーバーゴルーチンがWaitGroup
によって正確に追跡され、テストがsrvWG.Wait()
を呼び出したときに、すべてのサーバーゴルーチンが実際に終了するまで待機できるようになります。これは、テストのクリーンアップフェーズで、すべてのバックグラウンド処理が完了していることを保証するために不可欠です。
-
TestConcurrentReconnect
におけるsock.Close()
の戦略的な配置:- このテストでは、クライアントがメッセージを送信し終えた後 (
wg.Wait()
)、サーバーリスナーを明示的にクローズ (sock.Close()
) しています。これにより、runStreamSyslog
ゴルーチンがAccept()
からエラーを受け取り、速やかに終了できるようになります。 - その後、
srvWG.Wait()
を呼び出すことで、サーバーゴルーチンが完全に終了するのを待機します。この順序付けにより、リスナーがクローズされた後にAccept
がブロックされたままになるという競合状態が解消され、テストがデッドロックすることなく確実に終了するようになります。
- このテストでは、クライアントがメッセージを送信し終えた後 (
これらの変更は、テストコードにおけるリソースのライフサイクル管理と並行処理の同期を強化し、テストの信頼性と再現性を大幅に向上させました。
関連リンク
- Go Issue #4769: https://code.google.com/p/go/issues/detail?id=4769 (古いGoogle Codeのリンクですが、コミットメッセージに記載されています)
- Gerrit Change-Id:
7322089
- Gerrit Code Review: https://golang.org/cl/7322089
参考にした情報源リンク
- Go言語の公式ドキュメント:
sync
パッケージ: https://pkg.go.dev/syncnet
パッケージ: https://pkg.go.dev/netio
パッケージ: https://pkg.go.dev/io
- Go言語における並行処理と競合状態に関する一般的な情報源。
- Syslogプロトコルに関するRFCドキュメント (RFC 3164, RFC 5424)。