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

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

このコミットは、Go言語の標準ライブラリ net/http/httptest パッケージにおける httptest.ServerClose メソッドの挙動を改善するものです。具体的には、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リクエストが完了するのを待機しませんでした。

これにより、以下のような問題が発生する可能性がありました。

  1. 競合状態 (Race Condition): テストコードが httptest.Server を起動し、リクエストを送信した後、すぐに Server.Close() を呼び出すようなシナリオで、リクエストの処理が完了する前にサーバーがシャットダウンされてしまう可能性がありました。これにより、リクエストが途中で中断されたり、テストが不安定になったりする原因となります。
  2. リソースリークの可能性: 未完了のリクエストに関連するリソースが適切に解放されないままサーバーが終了してしまうことで、リソースリークやデッドロックのような問題を引き起こす可能性がありました。
  3. テストの信頼性の低下: テストが非決定的な結果を返す(パスしたり失敗したりする)原因となり、テストの信頼性を損ねていました。

コミットメッセージにある "Might fix issue 3050" については、Go言語の公式リポジトリにおける特定の公開されたIssue 3050は確認できませんでした。これは、内部的なバグトラッキングシステムにおける参照、あるいは当時まだ公開されていなかった問題への言及である可能性があります。しかし、このコミットの目的は、上記のような Server.Close の非同期的なシャットダウン挙動に起因する問題を解決することにあります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。

  1. net/http パッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための基本的な機能を提供するパッケージです。
    • http.Server: HTTPサーバーの構成(アドレス、ハンドラ、タイムアウトなど)を定義し、リクエストを処理するための構造体です。
    • http.Handler インターフェース: HTTPリクエストを処理するためのインターフェースで、ServeHTTP(ResponseWriter, *Request) メソッドを実装する必要があります。
    • http.ResponseWriter: HTTPレスポンスを構築するためにハンドラが使用するインターフェースです。
    • http.Request: クライアントからのHTTPリクエストを表す構造体です。
  2. net/http/httptest パッケージ: HTTPサーバーのテストを容易にするためのユーティリティを提供するパッケージです。
    • httptest.Server: テスト目的でHTTPサーバーを簡単に起動・停止できる構造体です。通常、ランダムなポートでリッスンし、テスト中にHTTPリクエストを処理します。
    • Server.Close(): httptest.Server をシャットダウンするためのメソッドです。このコミット以前は、未完了のリクエストを待機せずに即座にシャットダウンしていました。
  3. sync パッケージ: Go言語における並行処理の同期プリミティブを提供するパッケージです。
    • sync.WaitGroup: 複数のゴルーチンが完了するまで待機するために使用される同期プリミティブです。
      • Add(delta int): カウンタに delta を加算します。
      • Done(): カウンタを1減らします。通常は defer ステートメントで使用されます。
      • Wait(): カウンタがゼロになるまでブロックします。

技術的詳細

このコミットは、httptest.ServerClose メソッドが未完了のリクエストを待機するようにするために、sync.WaitGroup を導入しています。

変更の主要なポイントは以下の通りです。

  1. sync.WaitGroup の追加: httptest.Server 構造体に wg sync.WaitGroup フィールドが追加されました。この wg は、サーバー上で処理中のHTTPリクエストの数をカウントするために使用されます。

    type Server struct {
        // ...
        wg sync.WaitGroup
    }
    
  2. ハンドラのラップ (wrapHandler メソッドと waitGroupHandler 構造体): httptest.Server がリクエストを処理する際に、sync.WaitGroup のカウンタを増減させるための新しいハンドラが導入されました。

    • waitGroupHandler 構造体は、元の http.Handler をラップし、httptest.Server への参照 (s) を保持します。
    • waitGroupHandlerServeHTTP メソッドが呼び出されるたびに、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)
    }
    
  3. ハンドラの適用 (Start および StartTLS メソッド): httptest.ServerStart および 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,
        }
    }
    
  4. 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.HandlerwaitGroupHandler でラップするようになりました。
  • Start() および StartTLS() メソッド内で s.wrapHandler() が呼び出されるようになりました。
  • waitGroupHandler 構造体が追加されました。
  • waitGroupHandlerServeHTTP メソッドが実装され、sync.WaitGroupAdd(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 の変更とは直接的な依存関係はありませんが、テストの健全性を高めるための関連する改善です。

関連リンク

参考にした情報源リンク