[インデックス 14440] ファイルの概要
このコミットは、Go言語の公式ドキュメントに含まれるWikiアプリケーションのサンプルコードにおけるテストの競合状態(racy test)を修正するものです。具体的には、doc/articles/wiki/get.go
と doc/articles/wiki/test.bash
の2つのファイルが変更されています。
get.go
は、Wikiアプリケーションに対してHTTPリクエスト(GETまたはPOST)を行うクライアントユーティリティです。このコミットでは、サーバーが起動するのを待つための新しいオプション --wait_for_port
が追加され、リクエストが失敗した場合に指定された時間だけ再試行するロジックが導入されました。
test.bash
は、Wikiアプリケーションのテストスクリプトです。このスクリプトでは、Wikiサーバーをバックグラウンドで起動した後、すぐに get.go
を使ってリクエストを送信していました。以前は固定の sleep 1
コマンドでサーバーの起動を待っていましたが、このコミットでは get.go
の新しい --wait_for_port
オプションを使用するように変更され、より堅牢な待機メカニズムが導入されました。
コミット
commit 09f3c2f10f3336375975620c8ea47e03f2850c92
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Nov 19 12:36:15 2012 -0800
doc/articles/wiki: fix racy test
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6853069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/09f3c2f10f3336375975620c8ea47e03f2850c92
元コミット内容
doc/articles/wiki: fix racy test
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6853069
変更の背景
この変更の背景には、Go言語のドキュメントに含まれるWikiアプリケーションのサンプルコードのテストが、不安定な「競合状態(racy test)」に陥っていたという問題があります。
従来の test.bash
スクリプトでは、Wikiサーバーをバックグラウンドで起動した後、sleep 1
コマンドを使って1秒間待機してから、get.go
を使ってサーバーにリクエストを送信していました。しかし、この sleep 1
という固定の待機時間は、サーバーが実際にリクエストを受け付けられる状態になるまでの時間を保証するものではありませんでした。
考えられる問題点は以下の通りです。
- サーバーの起動時間のばらつき: サーバーの起動時間は、システムのリソース状況や他のプロセスの影響によって変動する可能性があります。1秒で起動しない場合、
get.go
がリクエストを送信した時点でサーバーがまだ準備できておらず、接続エラーやリクエスト失敗が発生し、テストが失敗する原因となります。 - 不必要な待機: 逆に、サーバーが1秒よりも早く起動した場合でも、テストは不必要に1秒間待機することになり、テスト全体の実行時間が長くなります。
このような固定時間による待機は、テストの信頼性を低下させ、CI/CD環境などでの自動テストにおいて、実際のバグがないにもかかわらずテストがランダムに失敗する「flaky test」の原因となります。このコミットは、この不安定なテストを修正し、サーバーが実際にリクエストを受け付けられるようになるまで待機する、より堅牢なメカニズムを導入することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念とGo言語の標準ライブラリに関する知識が必要です。
1. 競合状態 (Race Condition) と racy test
競合状態とは、複数のプロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。ソフトウェアテストの文脈では、「racy test」とは、テストの実行順序やタイミング、あるいは外部環境のわずかな変動によって、テストの成功/失敗が非決定的に変わるテストを指します。 今回のケースでは、Wikiサーバーの起動とクライアントからのリクエスト送信のタイミングが競合状態を引き起こしていました。サーバーが完全に起動する前にクライアントがリクエストを送ると、テストが失敗する可能性がありました。
2. Go言語の net/http
パッケージ
Go言語の標準ライブラリである net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。
http.Get(url string)
: 指定されたURLに対してHTTP GETリクエストを送信し、レスポンスを返します。http.Post(url string, contentType string, body io.Reader)
: 指定されたURLに対してHTTP POSTリクエストを送信します。contentType
はリクエストボディのMIMEタイプ(例: "application/x-www-form-urlencoded")、body
はリクエストボディのデータを含むio.Reader
です。
3. Go言語の flag
パッケージ
flag
パッケージは、コマンドライン引数を解析するための機能を提供します。
flag.String(name string, value string, usage string)
: 文字列型のコマンドラインフラグを定義します。flag.Bool(name string, value bool, usage string)
: 真偽値型のコマンドラインフラグを定義します。flag.Duration(name string, value time.Duration, usage string)
:time.Duration
型のコマンドラインフラグを定義します。これは時間間隔を表す型で、"5s" (5秒) や "1m" (1分) のような文字列で指定できます。flag.Parse()
: 定義されたフラグをコマンドライン引数から解析します。
4. Go言語の time
パッケージ
time
パッケージは、時間に関する機能を提供します。
time.Now()
: 現在の時刻を返します。time.Duration
: 時間の長さを表す型です。time.Time.Add(d Duration)
:Time
オブジェクトにDuration
を加算した新しいTime
オブジェクトを返します。time.Time.After(u Time)
:Time
オブジェクトが引数u
の時刻よりも後である場合にtrue
を返します。time.Sleep(d Duration)
: 指定されたDuration
だけ現在のゴルーチンをスリープさせます。
5. Bashスクリプトの基本
./final-test.bin &
:final-test.bin
という実行可能ファイルをバックグラウンドで実行します。&
を使うことで、スクリプトはfinal-test.bin
の終了を待たずに次のコマンドに進みます。sleep N
: N秒間スクリプトの実行を一時停止します。diff -u file1 file2
: 2つのファイルの差分をUnified形式で表示します。テストでは、生成された出力ファイルが期待される出力ファイルと一致するかどうかを確認するために使用されます。$addr
: Bashスクリプト内で変数の値を使用する際に$
を付けます。この場合、addr
変数にはWikiサーバーのアドレスが格納されています。
これらの知識が、コミットの変更内容とその意図を深く理解するための基盤となります。
技術的詳細
このコミットの技術的詳細は、クライアント側 (get.go
) とテストスクリプト側 (test.bash
) の両方で協調して競合状態を解決している点にあります。
get.go
の変更点
time
パッケージのインポート: 時間関連の処理を行うためにtime
パッケージが追加されました。--wait_for_port
フラグの追加:
新しいコマンドラインフラグvar ( // ... wait = flag.Duration("wait_for_port", 0, "if non-zero, the amount of time to wait for the address to become available") )
--wait_for_port
がtime.Duration
型で追加されました。このフラグは、HTTPリクエストを送信する前に、指定された時間だけサーバーが利用可能になるのを待機するための最大時間を設定します。デフォルト値は0
で、待機しないことを意味します。- リクエスト再試行ロジックの導入:
loopUntil := time.Now().Add(*wait) for { if *post != "" { b := strings.NewReader(*post) r, err = http.Post(url, "application/x-www-form-urlencoded", b) } else { r, err = http.Get(url) } if err == nil || *wait == 0 || time.Now().After(loopUntil) { break } time.Sleep(100 * time.Millisecond) }
main
関数内で、HTTPリクエスト(GETまたはPOST)を送信する部分がfor
ループで囲まれました。loopUntil := time.Now().Add(*wait)
: 待機を終了する時刻を計算します。これは現在の時刻に--wait_for_port
で指定された期間を加算したものです。for
ループ:- HTTPリクエストを試行します。
if err == nil || *wait == 0 || time.Now().After(loopUntil)
: この条件がtrue
になるとループを抜けます。err == nil
: リクエストが成功した場合。*wait == 0
:--wait_for_port
が指定されていない(待機しない)場合。time.Now().After(loopUntil)
: 待機時間が経過した場合。
time.Sleep(100 * time.Millisecond)
: リクエストが失敗し、かつ待機時間が残っている場合、100ミリ秒スリープしてから次の試行を行います。これにより、サーバーが起動するまでの間、ポーリング(定期的な再試行)が行われます。
test.bash
の変更点
- 固定
sleep
の削除:
不安定な-sleep 1
sleep 1
コマンドが削除されました。これにより、テストの実行がサーバーの実際の起動時間に依存するようになります。 --wait_for_port
フラグの使用:-./get.bin http://$addr/edit/Test > test_edit.out +./get.bin --wait_for_port=5s http://$addr/edit/Test > test_edit.out
get.bin
の呼び出しに--wait_for_port=5s
が追加されました。これは、get.bin
がWikiサーバーに接続できるまで最大5秒間待機することを指示します。これにより、サーバーが起動するまでの間、get.go
内部の再試行ロジックが機能し、テストがより堅牢になります。
これらの変更により、テストスクリプトはサーバーの起動を固定時間で待つのではなく、クライアントがサーバーに接続できるまで動的に待機するようになり、テストの信頼性が大幅に向上しました。
コアとなるコードの変更箇所
doc/articles/wiki/get.go
--- a/doc/articles/wiki/get.go
+++ b/doc/articles/wiki/get.go
@@ -13,11 +13,13 @@ import (
"net/http"
"os"
"strings"
+"time"
)
var (
post = flag.String("post", "", "urlencoded form data to POST")
addr = flag.Bool("addr", false, "find open address and print to stdout")
+"wait = flag.Duration("wait_for_port", 0, "if non-zero, the amount of time to wait for the address to become available")
)
func main() {
@@ -37,11 +39,18 @@ func main() {
}
var r *http.Response
var err error
- if *post != "" {
- b := strings.NewReader(*post)
- r, err = http.Post(url, "application/x-www-form-urlencoded", b)
- } else {
- r, err = http.Get(url)
+ loopUntil := time.Now().Add(*wait)
+ for {
+ if *post != "" {
+ b := strings.NewReader(*post)
+ r, err = http.Post(url, "application/x-www-form-urlencoded", b)
+ } else {
+ r, err = http.Get(url)
+ }
+ if err == nil || *wait == 0 || time.Now().After(loopUntil) {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
}
if err != nil {
log.Fatal(err)
doc/articles/wiki/test.bash
--- a/doc/articles/wiki/test.bash
+++ b/doc/articles/wiki/test.bash
@@ -18,9 +18,7 @@ go build -o final-test.bin final-test.go
(./final-test.bin) &
wiki_pid=$!
-sleep 1
-
-./get.bin http://$addr/edit/Test > test_edit.out
+./get.bin --wait_for_port=5s http://$addr/edit/Test > test_edit.out
diff -u test_edit.out test_edit.good
./get.bin -post=body=some%20content http://$addr/save/Test
diff -u Test.txt test_Test.txt.good
コアとなるコードの解説
doc/articles/wiki/get.go
の変更点
-
import "time"
の追加:time
パッケージは、時間に関する操作(現在時刻の取得、時間間隔の計算、スリープなど)を行うために必要です。新しい待機ロジックでtime.Now()
やtime.Duration
を使用するため、このインポートが追加されました。
-
wait
フラグの定義:wait = flag.Duration("wait_for_port", 0, "if non-zero, the amount of time to wait for the address to become available")
flag.Duration
を使用して、--wait_for_port
という新しいコマンドラインフラグを定義しています。このフラグはtime.Duration
型の値を期待し、サーバーが利用可能になるのを待つ最大時間を指定します。デフォルト値は0
で、待機しないことを意味します。これにより、get
コマンドがより柔軟になり、サーバーの起動を待つ必要があるシナリオで利用できるようになります。
-
リクエスト再試行ループの導入:
loopUntil := time.Now().Add(*wait) for { // ... HTTP リクエストの実行 ... if err == nil || *wait == 0 || time.Now().After(loopUntil) { break } time.Sleep(100 * time.Millisecond) }
loopUntil := time.Now().Add(*wait)
: これは、リクエストの再試行を停止する時刻を計算します。*wait
は--wait_for_port
フラグで指定された期間です。この時刻を過ぎると、たとえリクエストが成功していなくてもループを終了します。for
ループ: HTTPリクエストの実行を無限ループで囲んでいます。if err == nil || *wait == 0 || time.Now().After(loopUntil)
: この条件がtrue
になると、break
ステートメントによってループが終了します。err == nil
: HTTPリクエストがエラーなく成功した場合、それ以上待つ必要はないためループを抜けます。*wait == 0
:--wait_for_port
フラグが指定されていない(つまり、待機しない設定)場合、最初の試行で結果がどうであれループを抜けます。time.Now().After(loopUntil)
: 現在時刻がloopUntil
で計算された時刻を過ぎた場合、指定された最大待機時間を超えたため、エラーが発生していてもループを強制的に終了します。
time.Sleep(100 * time.Millisecond)
: リクエストが失敗し、かつまだ待機時間が残っている場合、100ミリ秒間スリープします。これにより、CPUを過度に消費することなく、短い間隔でサーバーの準備状況をポーリング(確認)できます。
この変更により、get.go
は単にHTTPリクエストを送信するだけでなく、サーバーが起動してリクエストを受け付けられるようになるまで、指定された時間内であれば自動的に再試行する「待機と再試行」の機能を持つようになりました。
doc/articles/wiki/test.bash
の変更点
-
sleep 1
の削除:-sleep 1
- これは、サーバーの起動を待つための固定の1秒間のスリープコマンドです。このコマンドは、サーバーの実際の起動時間に関わらず常に1秒間待機するため、テストの信頼性を低下させる原因となっていました。削除することで、より動的な待機メカニズムに置き換えられます。
-
get.bin
呼び出しへの--wait_for_port=5s
の追加:-./get.bin http://$addr/edit/Test > test_edit.out +./get.bin --wait_for_port=5s http://$addr/edit/Test > test_edit.out
get.bin
(get.go
をビルドして生成された実行ファイル)を呼び出す際に、新しく追加された--wait_for_port=5s
フラグを渡しています。これにより、get.bin
はWikiサーバーにHTTPリクエストを送信する前に、最大5秒間、サーバーが利用可能になるのを待機するようになります。この5秒間、get.go
内部の再試行ループが機能し、サーバーが起動するまでポーリングを行います。これにより、テストがサーバーの起動タイミングに依存せず、より堅牢で信頼性の高いものになります。
これらの変更は、テストの「競合状態」を解決し、テストの安定性を向上させるための重要なステップです。固定の sleep
に依存する代わりに、クライアント側でサーバーの準備状況を動的に確認するメカニズムを導入することで、テストがより堅牢になります。
関連リンク
- Go言語の
net/http
パッケージ: https://pkg.go.dev/net/http - Go言語の
flag
パッケージ: https://pkg.go.dev/flag - Go言語の
time
パッケージ: https://pkg.go.dev/time - Go Wiki (doc/articles/wiki の元となる記事): https://go.dev/doc/articles/wiki/
参考にした情報源リンク
- Go言語の公式ドキュメント
- Gitの差分表示
- 競合状態に関する一般的なプログラミングの知識
- Bashスクリプトの基本的なコマンド