[インデックス 12280] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http/httptest パッケージにおける httptest.Server の Close メソッドの挙動を改善するものです。具体的には、Server.Close が呼び出された際に、そのサーバーで処理中の未完了のリクエストがすべて終了するまで待機するように変更されます。これにより、テスト環境などにおいて、サーバーがシャットダウンされる前にリクエストが適切に処理されることが保証され、競合状態やテストの不安定性を解消する目的があります。
コミット
commit 8f0bfc5a29ab942af5b8dd1caf143383a90c2170
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 29 12:18:26 2012 -0800
net/http/httptest: make Server.Close wait for outstanding requests to finish
Might fix issue 3050
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5708066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8f0bfc5a29ab942af5b8dd1caf143383a90c2170
元コミット内容
net/http/httptest: make Server.Close wait for outstanding requests to finish
Might fix issue 3050
変更の背景
この変更の背景には、net/http/httptest パッケージの Server が、テストシナリオにおいて予期せぬ挙動を示す可能性があったという問題があります。以前の Server.Close メソッドは、リスナーを閉じるだけで、その時点で処理中のHTTPリクエストが完了するのを待機しませんでした。
これにより、以下のような問題が発生する可能性がありました。
- 競合状態 (Race Condition): テストコードが
httptest.Serverを起動し、リクエストを送信した後、すぐにServer.Close()を呼び出すようなシナリオで、リクエストの処理が完了する前にサーバーがシャットダウンされてしまう可能性がありました。これにより、リクエストが途中で中断されたり、テストが不安定になったりする原因となります。 - リソースリークの可能性: 未完了のリクエストに関連するリソースが適切に解放されないままサーバーが終了してしまうことで、リソースリークやデッドロックのような問題を引き起こす可能性がありました。
- テストの信頼性の低下: テストが非決定的な結果を返す(パスしたり失敗したりする)原因となり、テストの信頼性を損ねていました。
コミットメッセージにある "Might fix issue 3050" については、Go言語の公式リポジトリにおける特定の公開されたIssue 3050は確認できませんでした。これは、内部的なバグトラッキングシステムにおける参照、あるいは当時まだ公開されていなかった問題への言及である可能性があります。しかし、このコミットの目的は、上記のような Server.Close の非同期的なシャットダウン挙動に起因する問題を解決することにあります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
net/httpパッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための基本的な機能を提供するパッケージです。http.Server: HTTPサーバーの構成(アドレス、ハンドラ、タイムアウトなど)を定義し、リクエストを処理するための構造体です。http.Handlerインターフェース: HTTPリクエストを処理するためのインターフェースで、ServeHTTP(ResponseWriter, *Request)メソッドを実装する必要があります。http.ResponseWriter: HTTPレスポンスを構築するためにハンドラが使用するインターフェースです。http.Request: クライアントからのHTTPリクエストを表す構造体です。
net/http/httptestパッケージ: HTTPサーバーのテストを容易にするためのユーティリティを提供するパッケージです。httptest.Server: テスト目的でHTTPサーバーを簡単に起動・停止できる構造体です。通常、ランダムなポートでリッスンし、テスト中にHTTPリクエストを処理します。Server.Close():httptest.Serverをシャットダウンするためのメソッドです。このコミット以前は、未完了のリクエストを待機せずに即座にシャットダウンしていました。
syncパッケージ: Go言語における並行処理の同期プリミティブを提供するパッケージです。sync.WaitGroup: 複数のゴルーチンが完了するまで待機するために使用される同期プリミティブです。Add(delta int): カウンタにdeltaを加算します。Done(): カウンタを1減らします。通常はdeferステートメントで使用されます。Wait(): カウンタがゼロになるまでブロックします。
技術的詳細
このコミットは、httptest.Server の Close メソッドが未完了のリクエストを待機するようにするために、sync.WaitGroup を導入しています。
変更の主要なポイントは以下の通りです。
-
sync.WaitGroupの追加:httptest.Server構造体にwg sync.WaitGroupフィールドが追加されました。このwgは、サーバー上で処理中のHTTPリクエストの数をカウントするために使用されます。type Server struct { // ... wg sync.WaitGroup } -
ハンドラのラップ (
wrapHandlerメソッドとwaitGroupHandler構造体):httptest.Serverがリクエストを処理する際に、sync.WaitGroupのカウンタを増減させるための新しいハンドラが導入されました。waitGroupHandler構造体は、元のhttp.Handlerをラップし、httptest.Serverへの参照 (s) を保持します。waitGroupHandlerのServeHTTPメソッドが呼び出されるたびに、h.s.wg.Add(1)でWaitGroupのカウンタをインクリメントします。- リクエストの処理が完了するか、パニックが発生した場合でも確実にカウンタをデクリメントするために、
defer h.s.wg.Done()が使用されます。 - 実際のHTTPリクエスト処理は、ラップされた元のハンドラ
h.h.ServeHTTP(w, r)に委譲されます。
type waitGroupHandler struct { s *Server h http.Handler // non-nil } func (h *waitGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.s.wg.Add(1) defer h.s.wg.Done() // a defer, in case ServeHTTP below panics h.h.ServeHTTP(w, r) } -
ハンドラの適用 (
StartおよびStartTLSメソッド):httptest.ServerのStartおよびStartTLSメソッド内で、サーバーのConfig.Handlerが新しいwaitGroupHandlerでラップされるようになりました。これにより、すべてのHTTPリクエストがsync.WaitGroupによって追跡されるようになります。func (s *Server) Start() { // ... s.wrapHandler() // New line go s.Config.Serve(s.Listener) // ... } func (s *Server) StartTLS() { // ... s.wrapHandler() // New line go s.Config.Serve(s.Listener) } func (s *Server) wrapHandler() { h := s.Config.Handler if h == nil { h = http.DefaultServeMux } s.Config.Handler = &waitGroupHandler{ s: s, h: h, } } -
Server.Closeの待機:Server.Closeメソッドの最後にs.wg.Wait()が追加されました。これにより、Closeが呼び出されると、sync.WaitGroupのカウンタがゼロになる(つまり、すべての未完了のリクエストが処理を終える)まで、このメソッドはブロックされるようになります。func (s *Server) Close() { s.Listener.Close() s.wg.Wait() // New line }
この変更により、httptest.Server はより堅牢になり、テストの信頼性が向上します。特に、テストの終了時にサーバーが適切にクリーンアップされることが保証されるため、テストスイート全体の安定性に寄与します。
コアとなるコードの変更箇所
src/pkg/net/http/httptest/server.go
Server構造体にwg sync.WaitGroupフィールドが追加されました。wrapHandler()メソッドが追加され、Server.Config.HandlerをwaitGroupHandlerでラップするようになりました。Start()およびStartTLS()メソッド内でs.wrapHandler()が呼び出されるようになりました。waitGroupHandler構造体が追加されました。waitGroupHandlerのServeHTTPメソッドが実装され、sync.WaitGroupのAdd(1)とDone()を呼び出すようになりました。Server.Close()メソッドの最後にs.wg.Wait()が追加されました。
src/pkg/net/http/sniff_test.go
TestSniffWriteSizeテスト関数内で、Get関数から返されたレスポンスボディを閉じるためにres.Body.Close()が追加されました。これは、リソースリークを防ぐための一般的なベストプラクティスであり、Server.Closeの変更とは直接関係ありませんが、テストのクリーンアップを改善します。
コアとなるコードの解説
src/pkg/net/http/httptest/server.go
// Server 構造体への wg sync.WaitGroup の追加
type Server struct {
// ...
wg sync.WaitGroup
}
// wrapHandler メソッドの追加
// Server のハンドラを waitGroupHandler でラップし、リクエストの追跡を可能にする
func (s *Server) wrapHandler() {
h := s.Config.Handler
if h == nil {
h = http.DefaultServeMux // ハンドラが設定されていない場合はデフォルトのServeMuxを使用
}
s.Config.Handler = &waitGroupHandler{ // 新しいハンドラでラップ
s: s,
h: h,
}
}
// Start メソッドでの wrapHandler の呼び出し
func (s *Server) Start() {
// ...
s.URL = "http://" + s.Listener.Addr().String()
s.wrapHandler() // ここでハンドラをラップ
go s.Config.Serve(s.Listener) // ラップされたハンドラでサーバーを起動
// ...
}
// StartTLS メソッドでの wrapHandler の呼び出し
func (s *Server) StartTLS() {
// ...
s.URL = "https://" + s.Listener.Addr().String()
s.wrapHandler() // ここでハンドラをラップ
go s.Config.Serve(s.Listener) // ラップされたハンドラでサーバーを起動
}
// waitGroupHandler 構造体の定義
// HTTPリクエストの数をカウントし、Server.Close が待機できるようにする
type waitGroupHandler struct {
s *Server // 関連する httptest.Server へのポインタ
h http.Handler // ラップする元のハンドラ
}
// waitGroupHandler の ServeHTTP メソッドの実装
// 各リクエストの開始時に wg.Add(1) を呼び出し、終了時に wg.Done() を呼び出す
func (h *waitGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.s.wg.Add(1) // リクエスト開始時にカウンタをインクリメント
defer h.s.wg.Done() // リクエスト終了時(パニック時も含む)にカウンタをデクリメント
h.h.ServeHTTP(w, r) // 元のハンドラにリクエスト処理を委譲
}
// Server.Close メソッドの変更
// リスナーを閉じた後、未完了のリクエストがすべて終了するまで待機する
func (s *Server) Close() {
s.Listener.Close() // リスナーを閉じる
s.wg.Wait() // すべてのリクエストが完了するまで待機
}
src/pkg/net/http/sniff_test.go
func TestSniffWriteSize(t *testing.T) {
// ...
defer ts.Close()
for _, size := range []int{0, 1, 200, 600, 999, 1000, 1023, 1024, 512 << 10, 1 << 20} {
res, err := Get(fmt.Sprintf("%s/?size=%d", ts.URL, size))
if err != nil {
t.Fatalf("size %d: %v", size, err)
}
res.Body.Close() // レスポンスボディを閉じることでリソースを解放
}
}
この res.Body.Close() の追加は、HTTPクライアントがレスポンスボディを読み終えた後に明示的に閉じるという一般的なベストプラクティスに従ったものです。これにより、TCP接続が適切に再利用されたり、リソースがリークしたりするのを防ぎます。これは httptest.Server.Close の変更とは直接的な依存関係はありませんが、テストの健全性を高めるための関連する改善です。
関連リンク
- Go CL 5708066: https://golang.org/cl/5708066
参考にした情報源リンク
- Go言語
net/httpパッケージドキュメント: https://pkg.go.dev/net/http - Go言語
net/http/httptestパッケージドキュメント: https://pkg.go.dev/net/http/httptest - Go言語
syncパッケージドキュメント: https://pkg.go.dev/sync - Go issue 3050に関するWeb検索結果 (直接的な関連は確認できず、内部的な参照の可能性):
- Wailsapp/Wails GitHub Issue #3050: https://github.com/wailsapp/wails/issues/3050
- Red Hat OpenShift Dev Spaces (CRW-3050): https://access.redhat.com/documentation/en-us/red_hat_openshift_dev_spaces/3.1/html/release_notes/known-issues
- Go Runtime Source Code (runtime/proc.go line 3050): https://go.dev/src/runtime/proc.go?m=text#L3050
- Stack Overflow - Docker Networking (dial tcp 172.18.0.6:3050): https://stackoverflow.com/questions/64700000/dial-tcp-172-18-0-63050-connect-connection-refused-in-go-application-in-docker