[インデックス 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サーバーの起動方法と、テストスクリプトがサーバーのポート番号を取得する方法の変更です。
-
サーバー側の変更 (
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リクエストの処理を開始します。これにより、サーバーはバックグラウンドで起動し、ポート情報をファイルに書き込んだ後も動作し続けます。
-
テストスクリプト側の変更 (
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
変数を使用してサーバーに接続します。
- 以前は
-
Makefileの変更 (
Makefile
):CLEANFILES
変数にfinal-test.bin
の代わりにfinal.bin
とfinal-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.go
をsed
で変更してからビルドしていましたが、この変更により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言語の
net/http
パッケージ: https://pkg.go.dev/net/http - Go言語の
flag
パッケージ: https://pkg.go.dev/flag - Go言語の
net
パッケージ: https://pkg.go.dev/net - Go言語の
io/ioutil
パッケージ (Go 1.16以降はos
に統合): https://pkg.go.dev/io/ioutil - GitHub Issue #5564: https://github.com/golang/go/issues/5564
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコードリポジトリ
- Stack OverflowなどのプログラミングQ&Aサイト (一般的な競合状態やポート割り当てに関する情報)
git diff
コマンドの出力git log
コマンドの出力- Go言語のコミットメッセージの慣習
- Go言語のコードレビュープロセス (LGTM, R, CCなどの表記)
- Go言語のWikiアプリケーションのチュートリアル (変更前のコードの理解のため)