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

[インデックス 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 メソッドを呼び出した際、内部的にはリクエストの書き込みとレスポンスの読み込みが並行して行われる可能性があります。writeCrasherWrite メソッドは常にエラーを返しますが、Read メソッドがすぐに EOF を返してしまうと、クライアントは書き込みエラーを待つことなく読み込みエラー(EOF)を受け取ってしまうことがありました。これにより、テストが期待する「書き込みエラー」ではなく、「読み込みエラー」を検出してしまい、テストが不安定になる(flaky test)という問題が発生していました。

このコミットは、この不安定なテストを修正し、TestClientWriteError が常に意図したシナリオ(書き込みエラーの検出)を検証できるようにすることを目的としています。

前提知識の解説

Go言語の net/rpc パッケージ

net/rpc パッケージは、Go プログラム間でリモートプロシージャコール(RPC)を行うための標準ライブラリです。クライアントはサーバー上の公開されたメソッドを呼び出すことができ、サーバーはその呼び出しを受け取って処理し、結果をクライアントに返します。この通信は通常、ネットワーク接続(TCPなど)を介して行われます。

競合状態(Race Condition)

競合状態とは、複数のゴルーチン(またはスレッド)が共有リソースにアクセスする際に、そのアクセス順序によってプログラムの最終結果が変わってしまうバグの一種です。テストにおいては、特定のテストケースが実行されるたびに成功したり失敗したりする「不安定なテスト(flaky test)」の主な原因となります。今回のケースでは、RPC クライアントの書き込みと読み込みの処理が非同期に行われるため、writeCrasherRead メソッドが 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 メソッドは、リクエストを書き込み、レスポンスを読み込むという一連の処理を行います。この処理は内部的に非同期に行われる可能性があります。

  1. クライアントがリクエストの書き込みを開始し、writeCrasher.Writefake write failure を返します。
  2. 同時に、クライアントはレスポンスの読み込みも試みる可能性があります。
  3. 元の writeCrasher.Read は即座に io.EOF を返してしまうため、Write メソッドがエラーを返す前に Read メソッドが EOF を返してしまう競合状態が発生することがありました。
  4. この場合、c.Callfake write failure ではなく EOF エラーを返し、テストが失敗する(または意図しないパスを通る)ことになります。

この競合状態を解決するために、writeCrasherdone chan bool というチャネルが追加されました。

  • writeCrasher 構造体に done chan bool フィールドが追加され、NewClient に渡す際に初期化されます。
  • writeCrasher.Read メソッドは、<-w.done という行が追加され、done チャネルから値が送信されるまでブロックされるようになりました。
  • TestClientWriteError テストの最後に w.done <- true という行が追加され、テストが書き込みエラーを検証した後に done チャネルに値を送信するようになりました。

この変更により、以下のようになります。

  1. c.Call が呼び出され、writeCrasher.Writefake write failure を返します。
  2. writeCrasher.Readw.done チャネルからの受信を待つため、即座に EOF を返すことはありません。
  3. これにより、c.Call は確実に fake write failure を検出し、そのエラーを返します。
  4. テストは err.Error() != "fake write failure" を検証し、期待通りのエラーが返されたことを確認します。
  5. テストの最後に 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検索では直接的な情報が見つからなかったため、コミットメッセージの内容から推測)