[インデックス 11413] ファイルの概要
このコミットは、src/pkg/net/rpc/server_test.go
ファイルに対して行われた変更を記録しています。具体的には、TestClientWriteError
というテスト関数における競合状態(race condition)を修正するためのものです。
コミット
commit fa32b1641312f46b57ed8dbfdc83e0f726334a6a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 26 11:37:07 2012 +0400
net/rpc: fix race in TestClientWriteError test
Fixes #2752.
R=golang-dev, mpimenov, r
CC=golang-dev
https://golang.org/cl/5571062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fa32b1641312f46b57ed8dbfdc83e0f726334a6a
元コミット内容
net/rpc: fix race in TestClientWriteError test
Fixes #2752.
このコミットは、net/rpc
パッケージ内の TestClientWriteError
テストにおける競合状態を修正することを目的としています。これは、Go の issue #2752 に対応するものです。
変更の背景
net/rpc
パッケージは、Go 言語におけるリモートプロシージャコール(RPC)の実装を提供します。このパッケージのテストスイートには、クライアントが書き込みエラーを適切に処理するかどうかを検証する TestClientWriteError
というテストが含まれていました。
このテストは、writeCrasher
というカスタムの io.ReadWriteCloser
実装を使用しており、Write
メソッドが常にエラーを返すように設計されていました。しかし、元の実装では、Read
メソッドが即座に io.EOF
を返していました。これにより、テストの実行タイミングによっては、RPC クライアントが書き込みエラーを検出する前に、Read
メソッドが EOF
を返し、テストが意図しない結果(競合状態)となる可能性がありました。
具体的には、クライアントが Call
メソッドを呼び出した際、内部的にはリクエストの書き込みとレスポンスの読み込みが並行して行われる可能性があります。writeCrasher
の Write
メソッドは常にエラーを返しますが、Read
メソッドがすぐに EOF
を返してしまうと、クライアントは書き込みエラーを待つことなく読み込みエラー(EOF)を受け取ってしまうことがありました。これにより、テストが期待する「書き込みエラー」ではなく、「読み込みエラー」を検出してしまい、テストが不安定になる(flaky test)という問題が発生していました。
このコミットは、この不安定なテストを修正し、TestClientWriteError
が常に意図したシナリオ(書き込みエラーの検出)を検証できるようにすることを目的としています。
前提知識の解説
Go言語の net/rpc
パッケージ
net/rpc
パッケージは、Go プログラム間でリモートプロシージャコール(RPC)を行うための標準ライブラリです。クライアントはサーバー上の公開されたメソッドを呼び出すことができ、サーバーはその呼び出しを受け取って処理し、結果をクライアントに返します。この通信は通常、ネットワーク接続(TCPなど)を介して行われます。
競合状態(Race Condition)
競合状態とは、複数のゴルーチン(またはスレッド)が共有リソースにアクセスする際に、そのアクセス順序によってプログラムの最終結果が変わってしまうバグの一種です。テストにおいては、特定のテストケースが実行されるたびに成功したり失敗したりする「不安定なテスト(flaky test)」の主な原因となります。今回のケースでは、RPC クライアントの書き込みと読み込みの処理が非同期に行われるため、writeCrasher
の Read
メソッドが Write
メソッドのエラー発生よりも早く EOF
を返してしまうことで競合状態が発生していました。
io.Reader
および io.Writer
インターフェース
Go 言語の io
パッケージは、I/O 操作のための基本的なインターフェースを提供します。
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースで、データを読み込む機能を提供します。io.Writer
:Write(p []byte) (n int, err error)
メソッドを持つインターフェースで、データを書き込む機能を提供します。io.Closer
:Close() error
メソッドを持つインターフェースで、リソースを閉じる機能を提供します。io.ReadWriteCloser
: 上記3つのインターフェースを組み合わせたものです。
net/rpc
パッケージは、これらのインターフェースを使用して、基盤となるネットワーク接続を抽象化します。これにより、実際のネットワーク接続だけでなく、カスタムの io.ReadWriteCloser
実装(今回の writeCrasher
のようなもの)を使用してテストを行うことが可能になります。
Go のチャネル(chan
)
Go のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期にも使用できます。今回の修正では、バッファなしチャネル(make(chan bool)
)が使用されており、これは送信操作と受信操作が同時に行われるまでブロックされるため、ゴルーチン間の厳密な同期を保証します。
技術的詳細
元の TestClientWriteError
テストでは、writeCrasher
という構造体が io.ReadWriteCloser
インターフェースを実装していました。
Write
メソッドは常にfake write failure
というエラーを返します。これは、クライアントが書き込みエラーを処理できるかをテストするためです。Read
メソッドは、変更前はreturn 0, io.EOF
と即座にEOF
を返していました。
問題は、c.Call("foo", 1, &res)
が呼び出されたときに発生しました。Call
メソッドは、リクエストを書き込み、レスポンスを読み込むという一連の処理を行います。この処理は内部的に非同期に行われる可能性があります。
- クライアントがリクエストの書き込みを開始し、
writeCrasher.Write
がfake write failure
を返します。 - 同時に、クライアントはレスポンスの読み込みも試みる可能性があります。
- 元の
writeCrasher.Read
は即座にio.EOF
を返してしまうため、Write
メソッドがエラーを返す前にRead
メソッドがEOF
を返してしまう競合状態が発生することがありました。 - この場合、
c.Call
はfake write failure
ではなくEOF
エラーを返し、テストが失敗する(または意図しないパスを通る)ことになります。
この競合状態を解決するために、writeCrasher
に done chan bool
というチャネルが追加されました。
writeCrasher
構造体にdone chan bool
フィールドが追加され、NewClient
に渡す際に初期化されます。writeCrasher.Read
メソッドは、<-w.done
という行が追加され、done
チャネルから値が送信されるまでブロックされるようになりました。TestClientWriteError
テストの最後にw.done <- true
という行が追加され、テストが書き込みエラーを検証した後にdone
チャネルに値を送信するようになりました。
この変更により、以下のようになります。
c.Call
が呼び出され、writeCrasher.Write
がfake write failure
を返します。writeCrasher.Read
はw.done
チャネルからの受信を待つため、即座にEOF
を返すことはありません。- これにより、
c.Call
は確実にfake write failure
を検出し、そのエラーを返します。 - テストは
err.Error() != "fake write failure"
を検証し、期待通りのエラーが返されたことを確認します。 - テストの最後に
w.done <- true
が実行され、writeCrasher.Read
がブロック解除され、io.EOF
を返してクリーンアップされます。
この修正により、TestClientWriteError
は常に書き込みエラーのシナリオを正確にテストできるようになり、テストの不安定性が解消されました。
コアとなるコードの変更箇所
src/pkg/net/rpc/server_test.go
ファイルにおいて、以下の変更が行われました。
--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -467,13 +467,16 @@ func TestCountMallocsOverHTTP(t *testing.T) {
fmt.Printf("mallocs per HTTP rpc round trip: %d\n", countMallocs(dialHTTP, t))\n }\n \n-type writeCrasher struct{}\n+type writeCrasher struct {\n+\tdone chan bool\n+}\n \n func (writeCrasher) Close() error {\n \treturn nil\n }\n \n-func (writeCrasher) Read(p []byte) (int, error) {\n+func (w *writeCrasher) Read(p []byte) (int, error) {\n+\t<-w.done\n \treturn 0, io.EOF\n }\n \n@@ -482,7 +485,8 @@ func (writeCrasher) Write(p []byte) (int, error) {\n }\n \n func TestClientWriteError(t *testing.T) {\n-\tc := NewClient(writeCrasher{})\n+\tw := &writeCrasher{done: make(chan bool)}\n+\tc := NewClient(w)\n \tres := false\n \terr := c.Call(\"foo\", 1, &res)\n \tif err == nil {\n@@ -491,6 +495,7 @@ func TestClientWriteError(t *testing.T) {\n \tif err.Error() != \"fake write failure\" {\n \t\tt.Error(\"unexpected value of error:\", err)\n \t}\n+\tw.done <- true\n }\n \n func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {\n```
## コアとなるコードの解説
1. **`writeCrasher` 構造体の変更**:
```go
-type writeCrasher struct{}
+type writeCrasher struct {
+ done chan bool
+}
```
`writeCrasher` 構造体に `done` という `chan bool` 型のフィールドが追加されました。これは、`Read` メソッドの動作を制御するための同期プリミティブとして機能します。
2. **`writeCrasher.Read` メソッドの変更**:
```go
-func (writeCrasher) Read(p []byte) (int, error) {
+func (w *writeCrasher) Read(p []byte) (int, error) {
+ <-w.done
return 0, io.EOF
}
```
`Read` メソッドのレシーバが値レシーバ `(writeCrasher)` からポインタレシーバ `(w *writeCrasher)` に変更されました。これにより、`done` フィールドにアクセスできるようになります。
`<-w.done` という行が追加されました。これは、`w.done` チャネルから値が送信されるまで、この `Read` メソッドの実行をブロックします。これにより、テストが明示的に許可するまで `Read` が `io.EOF` を返すのを遅延させることができます。
3. **`TestClientWriteError` テスト関数の変更**:
```go
func TestClientWriteError(t *testing.T) {
- c := NewClient(writeCrasher{})
+ w := &writeCrasher{done: make(chan bool)}
+ c := NewClient(w)
res := false
err := c.Call("foo", 1, &res)
if err == nil {
t.Fatal("expected error, got nil")
}
if err.Error() != "fake write failure" {
t.Error("unexpected value of error:", err)
}
+ w.done <- true
}
```
- `writeCrasher` のインスタンス化方法が変更されました。`w := &writeCrasher{done: make(chan bool)}` となり、`done` チャネルが初期化された `writeCrasher` のポインタが作成されます。
- `NewClient` にはこの `w` ポインタが渡されます。
- テストの最後に `w.done <- true` という行が追加されました。これは、`c.Call` が期待される書き込みエラーを返したことを確認した後で、`done` チャネルに値を送信します。これにより、`writeCrasher.Read` がブロック解除され、テストが正常に終了できるようになります。
これらの変更により、`TestClientWriteError` は、RPC クライアントが書き込みエラーを処理するシナリオを、他の非同期処理(この場合は `Read` メソッドによる `EOF` の即時返却)の影響を受けずに、確実に検証できるようになりました。
## 関連リンク
- Go Gerrit Change-ID: [https://golang.org/cl/5571062](https://golang.org/cl/5571062)
## 参考にした情報源リンク
- コミットメッセージ自体 (`./commit_data/11413.txt`)
- Go 言語の `net/rpc` パッケージのドキュメント (一般的な知識として)
- Go 言語の `io` パッケージのドキュメント (一般的な知識として)
- Go 言語のチャネルに関するドキュメント (一般的な知識として)
- Go の issue #2752 (コミットメッセージに記載されているが、Web検索では直接的な情報が見つからなかったため、コミットメッセージの内容から推測)