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

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

このコミットは、Go言語の公式ドキュメントに含まれるWikiアプリケーションのテストにおける競合状態(race condition)を修正するものです。具体的には、テストがサーバーを起動し、そのサーバーが使用するポート番号をクライアントが取得する際の問題を解決しています。

コミット

commit 1f1f69e389a30fd8941789fd04bfd946c9aa39fc
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date:   Tue Mar 18 13:03:03 2014 +1100

    build: fix race in doc/articles/wiki test
    
    The original test would open a local port and then immediately close it
    and use the port number in subsequent tests. Between the port being closed
    and reused by the later process, it could be opened by some other program
    on the machine.
    
    Changed the test to run the server process directly and have it save the
    assigned port to a text file to be used by client processes.
    
    Fixes #5564.
    
    LGTM=adg
    R=golang-codereviews, gobot, adg
    CC=golang-codereviews
    https://golang.org/cl/72290043

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

https://github.com/golang/go/commit/1f1f69e389a30fd8941789fd04bfd946c9aa39fc

元コミット内容

このコミットは、doc/articles/wiki ディレクトリ内のテストにおける競合状態を修正します。元のテストでは、ローカルポートを開放し、すぐにそれを閉じてから、そのポート番号を後続のテストで使用していました。この「ポートが閉じられてから、後続のプロセスによって再利用されるまでの間」に、そのポートがマシン上の他のプログラムによって開かれてしまう可能性がありました。

この問題を解決するため、テストはサーバープロセスを直接実行し、割り当てられたポートをテキストファイルに保存するように変更されました。これにより、クライアントプロセスはそのファイルからポート番号を読み取って使用できるようになります。

この修正は、Issue #5564 に対応しています。

変更の背景

この変更の背景には、テストの信頼性の問題がありました。Go言語のドキュメントに含まれるWikiアプリケーションのテストは、ローカルでHTTPサーバーを起動し、そのサーバーと通信することで機能を確認していました。しかし、テストがサーバーを起動する際に、一時的にポートを確保し、そのポート番号を記憶してからサーバーを実際に起動するという手順を踏んでいました。この「ポートを確保して解放し、その番号を再利用する」というプロセスには、時間的なギャップが存在します。

この短いギャップの間に、システム上の別のプロセスが偶然にも同じポートを占有してしまう可能性がありました。もしそうなった場合、テストが期待するポートにWikiサーバーが起動できず、テストが失敗するという不安定な挙動を引き起こしていました。これは典型的な競合状態であり、テストが非決定論的になる原因となります。開発者は、このような不安定なテストを修正し、テストスイート全体の信頼性を向上させる必要がありました。

前提知識の解説

競合状態 (Race Condition)

競合状態とは、複数のプロセスやスレッドが共有リソース(この場合はネットワークポート)にアクセスする際に、そのアクセス順序によって結果が非決定論的になる状態を指します。今回のケースでは、テストプロセスがポートを解放した直後に、別のプロセスがそのポートを占有してしまう可能性があり、その結果、テストが意図した動作をできなくなるという問題が発生していました。

ネットワークポートとソケット

ネットワークポートは、IPアドレスと組み合わせて特定のアプリケーションやサービスを識別するために使用される番号です。TCP/IP通信において、アプリケーションは特定のポート番号に「バインド」することで、そのポートからの接続を待ち受けます。

net.Listen("tcp", "127.0.0.1:0") のようにポート番号を 0 に指定すると、オペレーティングシステムが利用可能なポートを自動的に割り当てます。これは、テストや一時的なサービスで特定のポート番号に依存したくない場合に非常に便利です。

flag パッケージ

Go言語の標準ライブラリである flag パッケージは、コマンドライン引数を解析するための機能を提供します。このコミットでは、final.go-addr という新しいフラグが追加され、サーバーが起動時に利用可能なポートを見つけてファイルに書き出すかどうかを制御するために使用されています。

net/http パッケージ

Go言語の net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。このコミットでは、WikiアプリケーションのHTTPサーバー部分がこのパッケージを利用しています。

ioutil.WriteFile

io/ioutil パッケージ(Go 1.16以降は os パッケージに統合)の WriteFile 関数は、指定されたファイルパスにバイトスライスを書き込むための便利な関数です。このコミットでは、割り当てられたポート番号をテキストファイルに保存するために使用されています。

技術的詳細

このコミットの主要な技術的変更点は、Wikiサーバーの起動方法と、テストスクリプトがサーバーのポート番号を取得する方法の変更です。

  1. サーバー側の変更 (final.go):

    • flag パッケージがインポートされ、-addr という新しいブーリアンフラグが定義されました。
    • main 関数内で flag.Parse() が呼び出され、コマンドライン引数が解析されます。
    • もし -addr フラグが true の場合、サーバーは特定のポート番号にバインドする代わりに、net.Listen("tcp", "127.0.0.1:0") を使用して利用可能なポートをオペレーティングシステムに自動的に割り当てさせます。
    • 割り当てられたポートのアドレス(例: 127.0.0.1:xxxxx)は、l.Addr().String() を介して取得されます。
    • このアドレスは ioutil.WriteFile を使用して final-port.txt というファイルに書き込まれます。
    • その後、サーバーは s.Serve(l) を使用して、このリスナー上でHTTPリクエストの処理を開始します。これにより、サーバーはバックグラウンドで起動し、ポート情報をファイルに書き込んだ後も動作し続けます。
  2. テストスクリプト側の変更 (test.bash):

    • 以前は get.bin -addr を実行してポート番号を取得していましたが、これは削除されました。
    • 代わりに、final.go をビルドした final.bin--addr フラグ付きで実行します: (./final.bin --addr) &。これにより、サーバーはバックグラウンドで起動し、final-port.txt にポート番号を書き込みます。
    • テストスクリプトは、final-port.txt ファイルが作成されるまでループで待機します。最大5秒間待機し、ファイルが作成されない場合はエラーで終了します。
    • ファイルが作成されたら、cat final-port.txt を使用してポート番号を読み取り、addr 変数に格納します。
    • 以降の get.bin コマンドは、この addr 変数を使用してサーバーに接続します。
  3. Makefileの変更 (Makefile):

    • CLEANFILES 変数に final-test.bin の代わりに final.binfinal-port.txt が追加されました。これは、新しいビルド成果物と一時ファイルをクリーンアップするためです。

これらの変更により、サーバーが実際に使用するポート番号をテストスクリプトに確実に伝えることができるようになり、競合状態が解消されました。

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

doc/articles/wiki/final.go

 // ... (既存のimport文)
+import (
+	"flag" // 新規追加
 	"html/template"
 	"io/ioutil"
+	"log" // 新規追加
+	"net" // 新規追加
 	"net/http"
 	"regexp"
 )
 
+var (
+	addr = flag.Bool("addr", false, "find open address and print to final-port.txt") // 新規追加
+)
+
 // ... (Page構造体、loadPage, savePage, renderTemplate, viewHandler, editHandler, saveHandler, makeHandler関数は変更なし)
 
 func main() {
+	flag.Parse() // 新規追加
 	http.HandleFunc("/view/", makeHandler(viewHandler))
 	http.HandleFunc("/edit/", makeHandler(editHandler))
 	http.HandleFunc("/save/", makeHandler(saveHandler))
+
+	if *addr { // 新規追加
+		l, err := net.Listen("tcp", "127.0.0.1:0") // 新規追加: 利用可能なポートをOSに割り当てさせる
+		if err != nil {
+			log.Fatal(err)
+		}
+		err = ioutil.WriteFile("final-port.txt", []byte(l.Addr().String()), 0644) // 新規追加: ポート情報をファイルに書き出す
+		if err != nil {
+			log.Fatal(err)
+		}
+		s := &http.Server{} // 新規追加
+		s.Serve(l) // 新規追加: リスナー上でサーバーを起動
+		return // 新規追加: ここで処理を終了し、通常のListenAndServeは実行しない
+	}
+
 	http.ListenAndServe(":8080", nil) // 既存: 通常のポート8080での起動
 }

doc/articles/wiki/test.bash

 # ... (既存のset -e, wiki_pid, cleanup関数, trap, if文)
 
 go build -o get.bin get.go
-addr=$(./get.bin -addr) # 削除: ポート取得方法の変更
-sed s/:8080/$addr/ < final.go > final-test.go # 削除: final.goの動的変更は不要に
-go build -o final-test.bin final-test.go # 削除: final-test.goのビルドは不要に
-(./final-test.bin) & # 削除: final-test.binの実行は不要に
+go build -o final.bin final.go # 変更: final.goを直接ビルド
+(./final.bin --addr) & # 変更: --addrフラグ付きでfinal.binをバックグラウンド実行
 wiki_pid=$!
 
-./get.bin --wait_for_port=5s http://$addr/edit/Test > test_edit.out # 変更: --wait_for_portは不要に
+l=0 # 新規追加: ループカウンタ
+while [ ! -f ./final-port.txt ] # 新規追加: final-port.txtが作成されるまで待機
+do
+\tl=$(($l+1))
+\tif [ "$l" -gt 5 ]
+\tthen
+\t\techo "port not available within 5 seconds"
+\t\texit 1
+\t\tbreak
+\tfi
+\tsleep 1
+done
+\
+addr=$(cat final-port.txt) # 新規追加: final-port.txtからポート番号を読み取る
+./get.bin 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 > test_save.out
 diff -u test_save.out test_view.good # should be the same as viewing

doc/articles/wiki/Makefile

 # ... (既存のallターゲット)
 
-CLEANFILES=get.bin final-test.bin a.out # 変更前
+CLEANFILES=get.bin final.bin a.out # 変更後: final-test.binをfinal.binに
 
 clean:
-	rm -f $(CLEANFILES) # 変更なし
+	rm -f $(CLEANFILES) final-port.txt # 変更後: final-port.txtもクリーンアップ対象に追加

コアとなるコードの解説

final.go の変更点

  • flag パッケージの導入: コマンドライン引数を処理するために flag パッケージがインポートされました。
  • -addr フラグの定義: var addr = flag.Bool("addr", false, "find open address and print to final-port.txt") により、--addr というブーリアン型のコマンドラインフラグが定義されました。このフラグが true の場合、特別なポート割り当てロジックが実行されます。
  • main 関数内の条件分岐:
    • flag.Parse() が呼び出され、コマンドライン引数が解析されます。
    • if *addr { ... } ブロックが追加されました。これは、--addr フラグが指定された場合にのみ実行されます。
    • 動的なポート割り当て: l, err := net.Listen("tcp", "127.0.0.1:0") は、OSに利用可能なTCPポートを自動的に割り当てさせるための重要な変更です。ポート番号を 0 にすることで、システムが未使用のポートを選択します。これにより、他のプロセスとのポート競合を避けることができます。
    • ポート情報のファイル書き出し: ioutil.WriteFile("final-port.txt", []byte(l.Addr().String()), 0644) は、割り当てられたポートのアドレス(例: 127.0.0.1:54321)を final-port.txt というファイルに書き込みます。このファイルは、テストスクリプトがサーバーのポートを知るための唯一の信頼できる情報源となります。
    • サーバーの起動: s := &http.Server{}; s.Serve(l) は、新しく作成されたリスナー l を使用してHTTPサーバーを起動します。これにより、サーバーはバックグラウンドで動作し続け、テストスクリプトがポート情報を読み取った後も利用可能になります。
    • 早期リターン: return ステートメントにより、--addr フラグが指定された場合は、通常の http.ListenAndServe(":8080", nil) の実行をスキップします。

test.bash の変更点

  • final.bin の直接実行: 以前は final.gosed で変更してからビルドしていましたが、この変更により final.go は直接 final.bin としてビルドされ、--addr フラグ付きで実行されます。これにより、サーバーはポート情報をファイルに書き出すモードで起動します。
  • ポートファイル待機ループ:
    • while [ ! -f ./final-port.txt ] ループが追加されました。これは、final-port.txt ファイルが作成されるまで1秒ごとにスリープしながら待機します。
    • l カウンタと if [ "$l" -gt 5 ] 条件により、最大5秒間のタイムアウトが設定されています。これにより、サーバーがポート情報を書き出すのに時間がかかりすぎたり、何らかの理由でファイルが作成されなかったりした場合に、テストが無限にハングアップするのを防ぎます。
  • ポート情報の読み取り: addr=$(cat final-port.txt) は、final-port.txt ファイルからサーバーが実際に使用しているポートアドレスを読み取り、addr シェル変数に格納します。
  • クライアントの接続: 以降の get.bin コマンドは、この addr 変数を使用して、サーバーが実際にリッスンしているアドレスに接続します。これにより、テストクライアントは常に正しいサーバーインスタンスと通信できるようになります。

これらの変更により、テストの実行順序とポート割り当てのタイミングに起因する競合状態が完全に排除され、テストの信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコードリポジトリ
  • Stack OverflowなどのプログラミングQ&Aサイト (一般的な競合状態やポート割り当てに関する情報)
  • git diff コマンドの出力
  • git log コマンドの出力
  • Go言語のコミットメッセージの慣習
  • Go言語のコードレビュープロセス (LGTM, R, CCなどの表記)
  • Go言語のWikiアプリケーションのチュートリアル (変更前のコードの理解のため)