[インデックス 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
: 関数がリターンする直前に実行されるステートメントをスケジュールします。リソースの解放(ファイルのクローズ、ロックの解除など)によく使われます。
- Goroutine(ゴルーチン): Go言語における軽量なスレッドのようなもので、
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')
は、指定されたデリミタ(ここでは改行)まで文字列を読み取ります。
技術的詳細
このコミットの主要な技術的変更は、テスト内の並行処理の同期を改善することにあります。
-
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
チャネルをクローズしたり、他のクリーンアップ処理を行ったりする前に、すべてのバックグラウンド処理が完了することが保証されます。
-
c.SetReadDeadline
の追加:runStreamSyslog
関数内の、各クライアント接続を処理するゴルーチンにc.SetReadDeadline(time.Now().Add(5 * time.Second))
が追加されました。- これは、クライアントからのデータ読み取り操作(
b.ReadString('\n')
)が最大5秒間だけブロックされるように設定します。もし5秒以内にデータが到着しない場合、読み取り操作はタイムアウトエラーを返します。 - この変更は、テスト中にクライアントが予期せず切断されたり、メッセージの送信を停止したりした場合に、サーバー側のゴルーチンが無限にブロックされ続けることを防ぎます。これにより、テストがハングアップする可能性が低減し、より堅牢になります。
これらの変更により、テストの実行がより予測可能になり、競合状態による不安定な失敗が解消されます。
コアとなるコードの変更箇所
src/pkg/log/syslog/syslog_test.go
ファイルにおける主要な変更箇所は以下の通りです。
-
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)\
-
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\
-
テスト関数での
startServer
の呼び出し箇所の更新:TestWithSimulated
,TestFlap
,TestWrite
,TestConcurrentWrite
関数内で、startServer
の戻り値がaddr, _
のように変更され、新しいwg
の戻り値が適切に処理されています。 -
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
チャネルをクローズする前に、すべてのバックグラウンド処理が完了し、競合状態が解消されます。
- このテストは、Syslogサーバーへの再接続を伴う並行書き込みをテストします。以前は、クライアント側のゴルーチンがすべて終了するのを
これらの変更は、Goの並行処理における「構造化された並行性(structured concurrency)」の原則に従い、ゴルーチンのライフサイクルを適切に管理することで、テストの信頼性と安定性を大幅に向上させています。
関連リンク
- Go言語公式ドキュメント:
sync
パッケージ: https://pkg.go.dev/syncnet
パッケージ: https://pkg.go.dev/netlog/syslog
パッケージ: https://pkg.go.dev/log/syslogtesting
パッケージ: https://pkg.go.dev/testing
- Go言語の並行処理に関するブログ記事(公式ブログなど):
- Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines
- Go Concurrency Patterns: Context: https://go.dev/blog/context
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- Go言語における並行処理とテストに関する一般的な知識
sync.WaitGroup
の使用パターンに関する一般的なプラクティス- ネットワークプログラミングにおけるタイムアウト処理の重要性