[インデックス 15411] ファイルの概要
このコミットは、Go言語の net/rpc
パッケージにおけるクライアントのシャットダウン処理に関する競合状態(race condition)と、エラーハンドリングの改善を目的としています。具体的には、Client
構造体のclosing
フラグが複数のゴルーチンから適切に保護されずにアクセスされる問題と、クライアントがシャットダウンされた後にClient.Call
がio.EOF
ではなくErrShutdown
を確実に返すように修正しています。また、関連するテストケースも追加されています。
コミット
Author: Roger Peppe rogpeppe@gmail.com Date: Mon Feb 25 16:22:00 2013 +0000
net/rpc: avoid racy use of closing flag.
It's accessed without mutex protection
in a different goroutine from the one that
sets it.
Also make sure that Client.Call after Client.Close
will reliably return ErrShutdown, and that clients
see ErrShutdown rather than io.EOF when appropriate.
Suggestions welcome for a way to reliably test
the mutex issue.
R=r, iant
CC=golang-dev
https://golang.org/cl/7338045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7edd13355f1a407a95c7a084c201867501f48ec6
元コミット内容
net/rpc: avoid racy use of closing flag.
It's accessed without mutex protection
in a different goroutine from the one that
sets it.
Also make sure that Client.Call after Client.Close
will reliably return ErrShutdown, and that clients
see ErrShutdown rather than io.EOF when appropriate.
Suggestions welcome for a way to reliably test
the mutex issue.
R=r, iant
CC=golang-dev
https://golang.org/cl/7338045
変更の背景
このコミットの背景には、Go言語の標準ライブラリであるnet/rpc
パッケージにおける2つの主要な問題がありました。
closing
フラグの競合状態:net/rpc
クライアントの内部状態を示すclosing
フラグが、複数のゴルーチンから同時にアクセスされる際に、適切な同期メカニズム(ミューテックスなど)によって保護されていませんでした。これにより、フラグの読み書きが非アトミックに行われ、予期せぬ動作やクラッシュを引き起こす可能性のある競合状態が発生していました。特に、Client.Close()
がclosing
フラグを設定するゴルーチンと、他のゴルーチンがそのフラグを読み取る際に問題が生じていました。- シャットダウン時のエラーハンドリングの不整合: クライアントがシャットダウンされた後、
Client.Call()
メソッドがnet/rpc
の内部エラーであるErrShutdown
を返すことが期待されます。しかし、実際には基盤となるネットワーク接続が閉じられた際に発生するio.EOF
(End Of File)エラーが返されることがありました。これは、クライアントが既にシャットダウン状態にあるにもかかわらず、あたかも通信の終端に達したかのような誤解を招くエラーであり、アプリケーション側での適切なエラーハンドリングを困難にしていました。このコミットは、シャットダウン時には常にErrShutdown
が返されるように、エラー変換ロジックを改善することを目的としています。
これらの問題は、net/rpc
クライアントの堅牢性と信頼性を低下させるものであり、特に高負荷環境やエラー発生時の挙動において予測不能な結果をもたらす可能性がありました。
前提知識の解説
Go言語のnet/rpc
パッケージ
net/rpc
は、Go言語の標準ライブラリに含まれるRPC(Remote Procedure Call)メカニズムを提供するパッケージです。これにより、異なるプロセスやネットワーク上のマシン間で、まるでローカル関数を呼び出すかのようにリモートの関数を呼び出すことができます。クライアントとサーバーのモデルで動作し、エンコーディングにはデフォルトでGoのgob
形式を使用しますが、他のコーデック(例: JSON-RPC)もサポートしています。
ゴルーチン(Goroutines)
ゴルーチンはGo言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。ゴルーチンはGoランタイムによってスケジューリングされ、チャネル(channels)を介して通信することで、安全な並行プログラミングを実現します。
ミューテックス(Mutexes)と競合状態(Race Conditions)
- ミューテックス:
sync.Mutex
は、共有リソースへのアクセスを同期するためのプリミティブです。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。これにより、一度に一つのゴルーチンのみが保護されたコードセクション(クリティカルセクション)を実行することを保証し、データ競合を防ぎます。 - 競合状態: 複数のゴルーチンが共有データに同時にアクセスし、少なくとも1つのゴルーチンがデータを変更する際に、アクセス順序によって結果が異なる場合に発生するバグです。競合状態はデバッグが困難であり、予測不能な動作を引き起こします。
io.EOF
とnet/rpc.ErrShutdown
io.EOF
:io
パッケージで定義されているエラーで、入力ストリームの終端に達したことを示します。通常、ファイルやネットワーク接続からの読み取りがこれ以上できない場合に返されます。net/rpc.ErrShutdown
:net/rpc
パッケージで定義されているエラーで、RPCクライアントまたはサーバーがシャットダウンされたことを示します。これは、RPCシステムが意図的に停止したことを示す、より具体的なエラーです。
これらのエラーは、発生する状況と意味合いが異なります。io.EOF
は通信の物理的な終端を示唆するのに対し、ErrShutdown
はRPCプロトコルレベルでの意図的な終了を示します。この区別は、アプリケーションがエラーを適切に処理し、シャットダウン状態を正確に認識するために重要です。
技術的詳細
このコミットは、net/rpc/client.go
ファイルに対して以下の主要な変更を加えています。
-
client.send
メソッドにおけるclosing
フラグの保護: 変更前:client.send
メソッド内で、client.shutdown
フラグのみがチェックされていました。 変更後:client.mutex.Lock()
で保護されたクリティカルセクション内で、client.shutdown || client.closing
という条件がチェックされるようになりました。これにより、client.closing
フラグへのアクセスもミューテックスによって保護され、競合状態が解消されます。client.closing
は、クライアントが閉じられつつあることを示すフラグであり、これがclient.mutex
によって保護されることで、Client.Close()
がこのフラグを設定する際に他のゴルーチンからの不正なアクセスを防ぎます。 -
client.input
メソッドにおけるエラーハンドリングの改善: 変更前:client.input
メソッド(RPC応答を読み取るゴルーチン)内で、client.codec.ReadResponseHeader
がio.EOF
を返した場合、client.closing
がfalse
であればio.ErrUnexpectedEOF
に変換されていました。これは、クライアントがまだ閉じられていないのにEOFを受け取った場合に予期せぬエラーとして扱うためのロジックでした。 変更後:io.EOF
の処理ロジックが変更され、より明確になりました。- 以前の
if err == io.EOF && !client.closing
のブロックは削除されました。 - 代わりに、
client.input
ゴルーチンが終了する直前(client.shutdown = true
を設定した後)に、err == io.EOF
の場合の処理が追加されました。 - この新しいロジックでは、
io.EOF
が発生した際に、closing
フラグがtrue
であればErrShutdown
に変換されます。これは、クライアントが意図的に閉じられている最中にEOFを受け取った場合は、シャットダウンエラーとして扱うべきであるという意図を反映しています。 closing
フラグがfalse
であれば、以前と同様にio.ErrUnexpectedEOF
に変換されます。これは、クライアントが閉じられていないのにEOFを受け取った場合は、予期せぬエラーとして扱うべきであるというロジックを維持しています。
- 以前の
これらの変更により、net/rpc
クライアントはより堅牢になり、シャットダウン時のエラー挙動が予測可能になりました。
また、src/pkg/net/rpc/server_test.go
には、TestErrorAfterClientClose
という新しいテストケースが追加されました。このテストは、クライアントがClose()
された後にCall()
を試みた際に、確実にErrShutdown
が返されることを検証します。これにより、上記のエラーハンドリングの改善が正しく機能していることが保証されます。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/rpc/client.go b/src/pkg/net/rpc/client.go
index ee3cc4d34d..4b0c9c3bba 100644
--- a/src/pkg/net/rpc/client.go
+++ b/src/pkg/net/rpc/client.go
@@ -71,7 +71,7 @@ func (client *Client) send(call *Call) {
// Register this call.
client.mutex.Lock()
- if client.shutdown {
+ if client.shutdown || client.closing {
call.Error = ErrShutdown
client.mutex.Unlock()
call.done()
@@ -105,9 +105,6 @@ func (client *Client) input() {
response = Response{}
err = client.codec.ReadResponseHeader(&response)
if err != nil {
- if err == io.EOF && !client.closing {
- err = io.ErrUnexpectedEOF
- }
break
}
seq := response.Seq
@@ -150,6 +147,13 @@ func (client *Client) input() {
client.mutex.Lock()
client.shutdown = true
closing := client.closing
+ if err == io.EOF {
+ if closing {
+ err = ErrShutdown
+ } else {
+ err = io.ErrUnexpectedEOF
+ }
+ }
for _, call := range client.pending {
call.Error = err
call.done()
diff --git a/src/pkg/net/rpc/server_test.go b/src/pkg/net/rpc/server_test.go
index db7778dcb2..8a15306235 100644
--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -524,6 +524,23 @@ func TestTCPClose(t *testing.T) {
}\n}\n \n+func TestErrorAfterClientClose(t *testing.T) {\n+\tonce.Do(startServer)\n+\n+\tclient, err := dialHTTP()\n+\tif err != nil {\n+\t\tt.Fatalf(\"dialing: %v\", err)\n+\t}\n+\terr = client.Close()\n+\tif err != nil {\n+\t\tt.Fatal(\"close error:\", err)\n+\t}\n+\terr = client.Call(\"Arith.Add\", &Args{7, 9}, new(Reply))\n+\tif err != ErrShutdown {\n+\t\tt.Errorf(\"Forever: expected ErrShutdown got %v\", err)\n+\t}\n+}\n+\n func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {\n \tb.StopTimer()\n \tonce.Do(startServer)\n```
## コアとなるコードの解説
### `src/pkg/net/rpc/client.go`
1. **`func (client *Client) send(call *Call)` の変更**:
```go
- if client.shutdown {
+ if client.shutdown || client.closing {
```
`send`メソッドは、RPC呼び出しを送信する前にクライアントの状態をチェックします。以前は`client.shutdown`(クライアントが完全にシャットダウンされた状態)のみをチェックしていましたが、この変更により`client.closing`(クライアントが閉じられつつある状態)もチェックするようになりました。`client.closing`フラグは`Client.Close()`メソッドによって設定されますが、このフラグへのアクセスがミューテックスで保護されていなかったため、競合状態の可能性がありました。この変更により、`client.mutex.Lock()`で保護されたクリティカルセクション内で`client.closing`もチェックされることで、`Client.Call`がシャットダウン中のクライアントに対して呼び出された際に、より早く`ErrShutdown`を返すようになり、競合状態を回避します。
2. **`func (client *Client) input()` の変更 (前半)**:
```go
- if err == io.EOF && !client.closing {
- err = io.ErrUnexpectedEOF
- }
```
このブロックは削除されました。以前は、`input`ゴルーチンがRPC応答ヘッダの読み取り中に`io.EOF`を受け取り、かつクライアントがまだ`closing`状態でない場合に、その`io.EOF`を`io.ErrUnexpectedEOF`に変換していました。このロジックは、後述の変更でより包括的なエラーハンドリングに置き換えられます。
3. **`func (client *Client) input()` の変更 (後半)**:
```go
client.mutex.Lock()
client.shutdown = true
closing := client.closing
+ if err == io.EOF {
+ if closing {
+ err = ErrShutdown
+ } else {
+ err = io.ErrUnexpectedEOF
+ }
+ }
for _, call := range client.pending {
call.Error = err
call.done()
```
`input`ゴルーチンが終了する際に、`client.shutdown`を`true`に設定し、保留中のすべてのRPC呼び出しにエラーを伝播します。この変更では、`input`ゴルーチンが終了する原因となったエラーが`io.EOF`であった場合の処理が追加されました。
* `closing`が`true`の場合(つまり、クライアントが意図的に閉じられている最中にEOFを受け取った場合)、エラーは`ErrShutdown`に変換されます。これにより、クライアントがシャットダウンされたことによる正常な終了として扱われ、アプリケーションは`ErrShutdown`を適切に処理できます。
* `closing`が`false`の場合(つまり、クライアントが閉じられていないのに予期せずEOFを受け取った場合)、エラーは`io.ErrUnexpectedEOF`に変換されます。これは、ネットワーク接続が予期せず切断されたなどの異常な状況を示します。
このロジックにより、`Client.Call`が`Client.Close`後に`ErrShutdown`を確実に返すようになり、エラーのセマンティクスが明確化されます。
### `src/pkg/net/rpc/server_test.go`
1. **`func TestErrorAfterClientClose(t *testing.T)` の追加**:
この新しいテストケースは、クライアントが`Close()`された後に`Call()`メソッドを呼び出した際に、期待通り`ErrShutdown`エラーが返されることを検証します。
* `dialHTTP()`でRPCクライアントを確立します。
* `client.Close()`を呼び出してクライアントを閉じます。
* 閉じられたクライアントに対して`client.Call()`を試みます。
* 返されたエラーが`ErrShutdown`であることをアサートします。
このテストは、`client.go`におけるエラーハンドリングの変更が正しく機能していることを確認するための重要な検証です。
## 関連リンク
* Go Code Review: [https://golang.org/cl/7338045](https://golang.org/cl/7338045)
## 参考にした情報源リンク
* 特になし(コミット内容とGo言語の一般的な知識に基づいています)