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

[インデックス 12004] ファイルの概要

このコミットは、Go言語の標準ライブラリであるnet/rpcパッケージ内のテストコードにおける、ベンチマーク実行時に発生する不必要なパニック(spurious panic)を修正するものです。具体的には、go test -benchtimeフラグを使用してベンチマークを実行した際に、テストが意図せずパニックを起こす問題を解決しています。

コミット

  • コミットハッシュ: 649f771b7b3538711bc8954c4a6f726d89c1226a
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Fri Feb 17 11:42:02 2012 +0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/649f771b7b3538711bc8954c4a6f726d89c1226a

元コミット内容

    net/rpc: fix spurious panic in test
    The panic happens if -benchtime flag is specified:
    go test -bench=EndToEndAsyncHTTP -benchtime=120
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5677075

変更の背景

このコミットの背景には、Go言語のnet/rpcパッケージのテストスイートにおいて、特定のベンチマークテスト(例: EndToEndAsyncHTTP)をgo test -benchtimeフラグと共に実行した際に、テストが「不必要なパニック(spurious panic)」を引き起こすという問題がありました。

go test -benchtimeフラグは、ベンチマークの実行時間を指定するために使用されます。例えば、-benchtime=120と指定すると、各ベンチマーク関数が最低120秒間実行されるように試みられます。

問題が発生していたWriteFailCodecというテスト用の構造体は、RPCのエンコーディング/デコーディング中にエラーをシミュレートするために使用されていました。この構造体のReadResponseHeaderメソッドとReadResponseBodyメソッドには、以前はtime.Sleep(120 * time.Second)というコードが含まれていました。これは、テストが長時間ブロックされることを意図していたと考えられます。

しかし、ベンチマークテストがbenchtimeフラグによって長時間実行されると、このtime.Sleepが原因で、テストのタイムアウトや、他のゴルーチンとの競合状態が発生し、最終的に「unreachable」とマークされたpanicステートメントが意図せず実行されてしまうという状況に陥っていました。このパニックは、テストの論理的な失敗を示すものではなく、テスト環境の特定の条件下で発生する「不必要な」ものであったため、修正が必要とされました。

前提知識の解説

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

net/rpcパッケージは、Go言語でリモートプロシージャコール(RPC)を実装するための標準ライブラリです。これにより、クライアントはネットワーク経由でリモートサーバー上の関数(メソッド)を、あたかもローカル関数であるかのように呼び出すことができます。

  • RPCの仕組み: クライアントはリモートのメソッドを呼び出し、その引数をシリアル化してサーバーに送信します。サーバーは引数をデシリアル化してメソッドを実行し、結果をシリアル化してクライアントに返します。
  • メソッドの要件: net/rpcで公開されるメソッドは、特定のシグネチャを持つ必要があります。通常、func (t *T) MethodName(argType T1, replyType *T2) errorのような形式で、最初の引数が入力、2番目の引数が結果へのポインタ、そしてerrorを返す必要があります。
  • エンコーディング: デフォルトでは、Goのencoding/gobパッケージを使用してデータのシリアル化が行われます。

go test -benchtimeフラグ

go testコマンドは、Goプログラムのテストとベンチマークを実行するためのツールです。 -benchtimeフラグは、ベンチマークの実行時間を制御するために使用されます。

  • 目的: ベンチマークが統計的に意味のある結果を出すために、十分な時間実行されることを保証します。特に、非常に高速な操作のベンチマークでは、実行回数を増やすことで測定誤差を減らすことができます。
  • 使用法: go test -bench=. -benchtime=5sのように、時間(例: 5s, 1m, 2h)を指定します。また、100xのように実行回数を指定することもできます。
  • デフォルト: デフォルトでは、各ベンチマークは最低1秒間実行されます。

select {}ステートメント

Go言語のselectステートメントは、複数の通信操作(チャネルの送受信)を待機するために使用されます。

  • 基本的な動作: selectは、いずれかのcaseが準備できるまでブロックします。複数のcaseが準備できた場合、ランダムに1つが選択されます。
  • select {}の意味: select {}は、case句を一切持たないselectステートメントです。これは、どのチャネル操作も指定されていないため、永遠にブロックし続けることを意味します。
  • 用途:
    • メインゴルーチンが終了するのを防ぎ、バックグラウンドで実行されている他のゴルーチンが動作し続けるようにする場合(例: サーバーアプリケーション)。
    • 意図的にゴルーチンをデッドロック状態にする場合(テストや特定の同期パターン)。
    • このコミットのケースのように、テストにおいて特定のコードパスが「到達不可能」であることを保証し、かつ無期限にブロックする必要がある場合。

技術的詳細

このコミットの技術的な核心は、WriteFailCodec構造体のReadResponseHeaderメソッドとReadResponseBodyメソッドにおけるtime.Sleep(120 * time.Second)の置き換えです。

元のコードでは、これらのメソッドは120秒間スリープした後、panic("unreachable")を実行していました。これは、これらのコードパスが通常のRPC操作では到達すべきではないことを示すためのものでした。しかし、go test -benchtimeフラグが指定され、ベンチマークが長時間実行されると、この120秒のスリープが問題を引き起こしました。

考えられる問題点:

  1. テストのタイムアウト: ベンチマークの実行時間がtime.Sleepの期間よりも短い場合、テストが完了する前にスリープが終了せず、テストがタイムアウトする可能性があります。
  2. リソースの占有: 120秒という長いスリープは、テスト実行中にリソースを不必要に占有し、他のテストやベンチマークの実行に影響を与える可能性があります。
  3. 競合状態: ベンチマーク環境では、複数のゴルーチンが並行して動作することが一般的です。time.Sleep中に他のゴルーチンが特定の状態に達し、このpanicステートメントが意図せずトリガーされるような競合状態が発生した可能性があります。コミットメッセージにある「spurious panic」という表現は、このパニックがテストの論理的な失敗ではなく、タイミングの問題や環境的な要因によって引き起こされたことを示唆しています。

新しいコードでは、time.Sleep(120 * time.Second)select {}に置き換えられています。

select {}は、前述の通り、永遠にブロックし続けるステートメントです。これにより、以下の利点が得られます。

  • 無期限のブロック: このメソッドが呼び出されると、そのゴルーチンはselect {}で無期限にブロックされます。これにより、panic("unreachable")ステートメントは絶対に実行されなくなります。これは、このコードパスが本当に到達不可能であることを保証する最も確実な方法です。
  • リソースの解放: time.Sleepとは異なり、select {}はCPUサイクルを消費しません。ゴルーチンはスケジューラによってブロック状態に置かれ、リソースを解放します。
  • 競合状態の回避: 長時間のスリープによるタイミングの問題や、それに起因する競合状態が根本的に解消されます。

この変更により、WriteFailCodecが使用されるテストにおいて、benchtimeフラグが指定されても不必要なパニックが発生しなくなり、テストの安定性が向上しました。

コアとなるコードの変更箇所

--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -387,12 +387,12 @@ func (WriteFailCodec) WriteRequest(*Request, interface{}) error {
 }
 
 func (WriteFailCodec) ReadResponseHeader(*Response) error {
-	time.Sleep(120 * time.Second)
+	select {}
 	panic("unreachable")
 }
 
 func (WriteFailCodec) ReadResponseBody(interface{}) error {
-	time.Sleep(120 * time.Second)
+	select {}
 	panic("unreachable")
 }
 

コアとなるコードの解説

変更されたファイルはsrc/pkg/net/rpc/server_test.goです。これはnet/rpcパッケージのテストファイルであり、RPCサーバーの動作を検証するためのテストコードが含まれています。

変更箇所は、WriteFailCodecというテスト用の構造体の2つのメソッドです。

  1. func (WriteFailCodec) ReadResponseHeader(*Response) error

    • 変更前: time.Sleep(120 * time.Second)
      • この行は、このメソッドが呼び出された際に、ゴルーチンを120秒間停止させていました。これは、RPCの応答ヘッダーの読み込みが非常に遅延するか、あるいは特定の条件下でブロックされる状況をシミュレートしようとしていた可能性があります。
    • 変更後: select {}
      • この行は、ゴルーチンを無期限にブロックします。これにより、このメソッドが呼び出されると、そのゴルーチンは永遠に停止し、その後のpanic("unreachable")ステートメントは決して実行されなくなります。これは、このコードパスがテストの意図として「到達不可能」であることをより確実に保証します。
  2. func (WriteFailCodec) ReadResponseBody(interface{}) error

    • 変更前: time.Sleep(120 * time.Second)
      • 上記と同様に、RPCの応答ボディの読み込みが遅延する状況をシミュレートしていました。
    • 変更後: select {}
      • 上記と同様に、ゴルーチンを無期限にブロックし、panic("unreachable")が実行されないようにします。

この変更の意図は、WriteFailCodecがRPC通信の特定のフェーズで「失敗」または「ブロック」することをシミュレートする際に、time.Sleepによる固定的な遅延がベンチマークの実行時間と競合し、意図しないパニックを引き起こす問題を解決することです。select {}を使用することで、このテスト用の失敗シミュレーションが、ベンチマークの実行時間に関わらず、安定して「到達不可能」な状態を維持できるようになりました。

関連リンク

参考にした情報源リンク