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

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

このコミットは、Go言語の公式ドキュメントに含まれるWikiアプリケーションのサンプルコードにおけるテストの競合状態(racy test)を修正するものです。具体的には、doc/articles/wiki/get.godoc/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. サーバーの起動時間のばらつき: サーバーの起動時間は、システムのリソース状況や他のプロセスの影響によって変動する可能性があります。1秒で起動しない場合、get.go がリクエストを送信した時点でサーバーがまだ準備できておらず、接続エラーやリクエスト失敗が発生し、テストが失敗する原因となります。
  2. 不必要な待機: 逆に、サーバーが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 の変更点

  1. time パッケージのインポート: 時間関連の処理を行うために time パッケージが追加されました。
  2. --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_porttime.Duration 型で追加されました。このフラグは、HTTPリクエストを送信する前に、指定された時間だけサーバーが利用可能になるのを待機するための最大時間を設定します。デフォルト値は 0 で、待機しないことを意味します。
  3. リクエスト再試行ロジックの導入:
    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 の変更点

  1. 固定 sleep の削除:
    -sleep 1
    
    不安定な sleep 1 コマンドが削除されました。これにより、テストの実行がサーバーの実際の起動時間に依存するようになります。
  2. --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 の変更点

  1. import "time" の追加:

    • time パッケージは、時間に関する操作(現在時刻の取得、時間間隔の計算、スリープなど)を行うために必要です。新しい待機ロジックで time.Now()time.Duration を使用するため、このインポートが追加されました。
  2. 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 コマンドがより柔軟になり、サーバーの起動を待つ必要があるシナリオで利用できるようになります。
  3. リクエスト再試行ループの導入:

    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 の変更点

  1. sleep 1 の削除:

    -sleep 1
    
    • これは、サーバーの起動を待つための固定の1秒間のスリープコマンドです。このコマンドは、サーバーの実際の起動時間に関わらず常に1秒間待機するため、テストの信頼性を低下させる原因となっていました。削除することで、より動的な待機メカニズムに置き換えられます。
  2. 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.binget.go をビルドして生成された実行ファイル)を呼び出す際に、新しく追加された --wait_for_port=5s フラグを渡しています。これにより、get.bin はWikiサーバーにHTTPリクエストを送信する前に、最大5秒間、サーバーが利用可能になるのを待機するようになります。この5秒間、get.go 内部の再試行ループが機能し、サーバーが起動するまでポーリングを行います。これにより、テストがサーバーの起動タイミングに依存せず、より堅牢で信頼性の高いものになります。

これらの変更は、テストの「競合状態」を解決し、テストの安定性を向上させるための重要なステップです。固定の sleep に依存する代わりに、クライアント側でサーバーの準備状況を動的に確認するメカニズムを導入することで、テストがより堅牢になります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Gitの差分表示
  • 競合状態に関する一般的なプログラミングの知識
  • Bashスクリプトの基本的なコマンド