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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の ConnState (コネクションの状態管理) の挙動を修正するものです。具体的には、StateHijackedStateActive という2つのコネクション状態の遷移タイミングと条件を改善しています。

変更が加えられたファイルは以下の2つです。

  • src/pkg/net/http/serve_test.go: net/http パッケージのテストファイル。今回の修正によって影響を受ける ConnState の挙動を検証するための新しいテストケースが追加されています。特に、Hijack 後のパニック発生時の StateHijacked の遷移や、StateActive の厳密な条件をテストしています。
  • src/pkg/net/http/server.go: net/http パッケージのHTTPサーバー実装のコアファイル。ConnState の遷移ロジック、特に hijack メソッドと serve メソッド内の状態設定が変更されています。

コミット

commit 709b12ffe63b6f13a344286f9ff6afed7c283887
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Mar 3 18:58:28 2014 -0800

    net/http: fix location of StateHijacked and StateActive
    
    1) Move StateHijacked callback earlier, to make it
    panic-proof.  A Hijack followed by a panic didn't previously
    result in ConnState getting fired for StateHijacked.  Move it
    earlier, to the time of hijack.
    
    2) Don't fire StateActive unless any bytes were read off the
    wire while waiting for a request. This means we don't
    transition from New or Idle to Active if the client
    disconnects or times out. This was documented before, but not
    implemented properly.
    
    This CL is required for an pending fix for Issue 7264
    
    LGTM=josharian
    R=josharian
    CC=golang-codereviews
    https://golang.org/cl/69860049

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

https://github.com/golang/go/commit/709b12ffe63b6f13a344286f9ff6afed7c283887

元コミット内容

net/http: fix location of StateHijacked and StateActive

  1. StateHijacked コールバックを早期に移動し、パニック耐性を持たせる。以前は Hijack の後にパニックが発生した場合、ConnStateStateHijacked を発火しなかった。これを hijack 時点に早期移動する。
  2. リクエストを待っている間にワイヤーからバイトが読み取られない限り、StateActive を発火させない。これは、クライアントが切断またはタイムアウトした場合に、New または Idle から Active へ遷移しないことを意味する。これは以前から文書化されていたが、適切に実装されていなかった。

この変更は、Issue 7264 の保留中の修正に必要である。

変更の背景

このコミットは、Goの net/http パッケージにおけるHTTPコネクションの状態管理 (ConnState) の挙動に関する2つの主要な問題を解決するために行われました。

  1. StateHijacked の信頼性の向上: net/http パッケージには、HTTPコネクションをHTTPサーバーの制御から「ハイジャック」し、低レベルのTCPコネクションを直接操作する機能があります。これはWebSocketのようなプロトコルを実装する際に利用されます。しかし、以前の実装では、コネクションがハイジャックされた直後にサーバー側でパニックが発生した場合、ConnState のコールバックが StateHijacked 状態を適切に通知しないという問題がありました。これにより、コネクションの状態を正確に追跡しているアプリケーションで問題が発生する可能性がありました。このコミットは、StateHijacked の通知をハイジャック処理のより早い段階に移動することで、このパニック耐性の問題を解決しようとしています。

  2. StateActive の厳密な適用: ConnStateStateActive は、コネクションがアクティブにリクエストを処理している状態を示すものです。net/http のドキュメントでは、この状態は「ワイヤーからバイトが読み取られた場合」にのみ遷移するとされていました。しかし、実際の実装では、クライアントがリクエストを送信せずに切断したり、タイムアウトしたりした場合でも、不適切に StateActive に遷移してしまうことがありました。これは、コネクションの状態管理の正確性を損ない、特にコネクションプーリングやリソース管理を行うアプリケーションにとって混乱を招く可能性がありました。このコミットは、StateActive への遷移条件を厳密化し、実際にデータが読み取られた場合にのみ遷移するように修正することで、ドキュメントとの整合性を図っています。

これらの修正は、GoのHTTPサーバーの堅牢性と正確性を向上させることを目的としており、特に「Issue 7264」という特定のバグ修正に関連していることが明記されています。

前提知識の解説

このコミットの技術的詳細を理解するためには、以下のGo言語の net/http パッケージに関する前提知識が必要です。

  1. net/http パッケージ: Go言語の標準ライブラリであり、HTTPクライアントとサーバーの実装を提供します。Webアプリケーション開発において中心的な役割を果たします。

  2. http.Server: HTTPサーバーを構成および実行するための構造体です。ListenAndServe メソッドを通じてリクエストを待ち受け、処理します。

  3. http.ConnState: http.Server の設定オプションの一つで、コネクションの状態変化を監視するためのコールバック関数を設定できます。このコールバックは、net.Conn オブジェクトと http.ConnState 型の列挙値を受け取ります。http.ConnState は以下の状態を定義します。

    • http.StateNew: 新しいコネクションが確立された直後の状態。
    • http.StateActive: コネクションがリクエストを読み取っている、またはレスポンスを書き込んでいる状態。
    • http.StateIdle: コネクションがアイドル状態であり、次のリクエストを待っている状態(Keep-Aliveコネクションの場合)。
    • http.StateHijacked: コネクションがHTTPサーバーの制御から「ハイジャック」され、アプリケーションが直接TCPコネクションを操作している状態。
    • http.StateClosed: コネクションが閉じられた状態。
  4. http.Hijacker インターフェース: http.ResponseWriter が実装できるインターフェースで、Hijack() メソッドを提供します。このメソッドを呼び出すことで、HTTPサーバーは基盤となるTCPコネクションの制御をアプリケーションに引き渡します。これにより、WebSocketのようなHTTP以外のプロトコルを同じポートで処理することが可能になります。Hijack() が呼び出されると、net.Connbufio.ReadWriter が返され、アプリケーションはこれらを使って低レベルのネットワークI/Oを直接行います。

  5. bufio.Readerbufio.Writer: バッファリングされたI/Oを提供するGoの標準ライブラリです。ネットワークI/Oの効率を高めるために net/http パッケージ内で広く利用されています。Hijack() メソッドが *bufio.ReadWriter を返すのは、HTTPリクエストの読み取り途中に残されたバッファ内のデータをアプリケーションが引き続き利用できるようにするためです。

  6. パニック (Panic): Goにおけるランタイムエラーの一種です。パニックが発生すると、通常のプログラムフローは中断され、defer関数が実行された後、プログラムは終了します。HTTPサーバーのような長時間稼働するサービスでは、パニックは予期せぬシャットダウンを引き起こすため、適切に処理される必要があります。

  7. io.LimitedReader: io.Reader をラップし、読み取ることができるバイト数を制限するリーダーです。net/http サーバーでは、リクエストヘッダーの最大サイズを制限するために使用されます。これにより、悪意のある大きなヘッダーによるサービス拒否攻撃を防ぎます。

これらの概念を理解することで、コミットが ConnState の挙動をどのように改善し、HTTPサーバーの堅牢性と正確性を高めているかを深く把握できます。

技術的詳細

このコミットは、net/http パッケージにおけるコネクションの状態管理、特に StateHijackedStateActive の遷移ロジックを修正することで、HTTPサーバーの信頼性と正確性を向上させています。

1. StateHijacked の遷移タイミングの修正

問題点: 以前の実装では、http.ResponseWriterHijack() メソッドが呼び出された後、ConnState コールバックが StateHijacked を通知する前に、サーバーのハンドラ内でパニックが発生する可能性がありました。この場合、StateHijacked の通知がスキップされ、コネクションの状態を正確に追跡している外部システム(例えば、コネクションプーリングや監視システム)が誤った情報を保持することになります。

修正内容: src/pkg/net/http/server.go(*conn).hijack() メソッド内で、コネクションがハイジャックされた直後に c.setState(rwc, StateHijacked) を呼び出すように変更されました。 以前は、(*conn).serve() メソッドの serverHandler{c.server}.ServeHTTP(w, w.req) の呼び出し後に c.setState(origConn, StateHijacked) が行われていました。この変更により、Hijack() が成功した時点で即座に StateHijacked が通知されるようになり、その後のハンドラ内でのパニックによって状態通知が失われるリスクがなくなりました。これにより、StateHijacked の通知がより堅牢で信頼性の高いものになりました。

2. StateActive の遷移条件の厳密化

問題点: ConnState のドキュメントでは、StateActive は「コネクションがリクエストを読み取っている、またはレスポンスを書き込んでいる状態」と定義されており、特に「ワイヤーからバイトが読み取られた場合」に遷移するとされていました。しかし、実際の実装では、クライアントがリクエストデータを送信する前に切断したり、タイムアウトしたりした場合でも、StateNewStateIdle から不適切に StateActive に遷移してしまうことがありました。これは、コネクションが実際にアクティブなデータ転送を行っていないにもかかわらず、StateActive と報告されるという矛盾を生んでいました。

修正内容: src/pkg/net/http/server.go(*conn).serve() メソッド内で、StateActive への遷移条件が変更されました。 以前は、c.readRequest() の呼び出し後に無条件に c.setState(c.rwc, StateActive) が呼び出されていました。 新しい実装では、c.lr.N != c.server.initialLimitedReaderSize() という条件が追加されました。

  • c.lrio.LimitedReader であり、リクエストヘッダーの読み取りに使用されます。
  • c.server.initialLimitedReaderSize() は、io.LimitedReader が初期状態で読み取れる最大バイト数(maxHeaderBytes + bufio slop)を返します。
  • この条件は、c.readRequest() が呼び出された後、io.LimitedReader が初期サイズから減少している(つまり、実際にワイヤーから何らかのバイトが読み取られた)場合にのみ StateActive に遷移するようにします。
    • もし c.lr.NinitialLimitedReaderSize() と同じままであれば、それはリクエストヘッダーの読み取り中に何もバイトが読み取られなかったことを意味します(例: クライアントが接続後すぐに切断した、またはタイムアウトした)。この場合、StateActive には遷移しません。
    • これにより、コネクションが実際にデータ転送を行っている場合にのみ StateActive が通知されるようになり、ドキュメントとの整合性が保たれ、より正確な状態管理が可能になりました。

Issue 7264 との関連

コミットメッセージには「This CL is required for an pending fix for Issue 7264」と記載されています。公開されているGoのIssueトラッカーで「Issue 7264」を直接特定することはできませんでしたが、この記述から、このコミットが解決する問題(StateHijacked のパニック耐性や StateActive の厳密な適用)が、より広範なバグ修正または改善の一部であることが示唆されます。おそらく、特定のシナリオで net/http サーバーが予期せぬ動作をする問題があり、その根本原因が ConnState の不正確な遷移にあったと考えられます。

これらの変更は、net/http パッケージの堅牢性を高め、開発者がコネクションの状態をより正確に把握し、それに基づいてアプリケーションのロジックを構築できるようにすることを目的としています。

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

src/pkg/net/http/server.go

  1. (*conn).hijack() メソッドの変更:

    // 変更前:
    // (StateHijacked の設定は serve() メソッド内で行われていた)
    
    // 変更後:
    func (c *conn) hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
        // ... (既存のコード) ...
        c.setState(rwc, StateHijacked) // ここに移動
        return
    }
    

    StateHijacked の設定が、hijack() メソッド内でコネクションがハイジャックされた直後に行われるようになりました。

  2. (*Server).initialLimitedReaderSize() メソッドの追加:

    // 追加:
    func (srv *Server) initialLimitedReaderSize() int64 {
        return int64(srv.maxHeaderBytes()) + 4096 // bufio slop
    }
    

    io.LimitedReader の初期サイズを計算するためのヘルパーメソッドが追加されました。

  3. (*conn).readRequest() メソッドの変更:

    // 変更前:
    // c.lr.N = int64(c.server.maxHeaderBytes()) + 4096 /* bufio slop */
    
    // 変更後:
    func (c *conn) readRequest() (w *response, err error) {
        // ... (既存のコード) ...
        c.lr.N = c.server.initialLimitedReaderSize() // 新しいヘルパーメソッドを使用
        // ... (既存のコード) ...
    }
    

    io.LimitedReader の初期サイズ設定に、新しく追加された initialLimitedReaderSize() メソッドが使用されるようになりました。

  4. (*conn).serve() メソッドの変更:

    // 変更前:
    // c.setState(c.rwc, StateActive) // 無条件に StateActive に遷移
    
    // 変更後:
    func (c *conn) serve() {
        // ... (既存のコード) ...
        for {
            w, err := c.readRequest()
            if c.lr.N != c.server.initialLimitedReaderSize() { // 条件を追加
                // If we read any bytes off the wire, we're active.
                c.setState(c.rwc, StateActive)
            }
            // ... (既存のコード) ...
            if c.hijacked() {
                // c.setState(origConn, StateHijacked) // ここから削除
                return
            }
            // ... (既存のコード) ...
        }
    }
    

    StateActive への遷移に条件が追加され、StateHijacked の設定が削除されました。

src/pkg/net/http/serve_test.go

  1. TestServerConnState 関数の変更:
    • "/hijack-panic" という新しいハンドラが追加されました。このハンドラは Hijack() を呼び出した後、意図的にパニックを発生させます。これは StateHijacked のパニック耐性をテストするためです。
    • ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) が追加され、テスト中のエラーログ出力を抑制しています。
    • 新しいテストケースが追加され、StateNew -> StateClosedStateNew -> StateActive -> StateClosedStateNew -> StateActive -> StateIdle -> StateClosed といった様々なコネクション状態遷移を検証しています。これらは StateActive の厳密な条件をテストするためのものです。
    • want マップに、新しいテストケースに対応する期待される ConnState のシーケンスが追加されました。特に、StateHijacked がパニック発生時にも適切に記録されること、および StateActive が実際にバイトが読み取られた場合にのみ遷移することを確認しています。

コアとなるコードの解説

src/pkg/net/http/server.go

  • (*conn).hijack() メソッド: このメソッドは、HTTPサーバーがクライアントとのコネクションの制御をアプリケーションに引き渡す際に呼び出されます。以前は、StateHijacked の通知は serve() メソッドの後半で行われていました。しかし、このコミットでは、c.setState(rwc, StateHijacked) の呼び出しが hijack() メソッドの内部、つまりコネクションの制御が引き渡された直後に移動されました。 この変更の意図は、Hijack() が成功した時点でコネクションがハイジャックされた状態であることを確実に ConnState コールバックに通知することです。これにより、Hijack() 呼び出し後にハンドラ内でパニックが発生した場合でも、StateHijacked の通知が失われることがなくなり、コネクションの状態追跡の信頼性が向上します。

  • (*Server).initialLimitedReaderSize() メソッド: これは新しいヘルパー関数で、http.Server がリクエストヘッダーを読み取る際に使用する io.LimitedReader の初期サイズを計算します。このサイズは、MaxHeaderBytes (最大ヘッダーサイズ) に bufio の内部バッファリングによる「スロップ」(余分なスペース)を加えたものです。この関数を導入することで、マジックナンバーを排除し、コードの可読性と保守性を向上させています。

  • (*conn).readRequest() メソッド: このメソッドは、クライアントからのHTTPリクエストを読み取ります。変更点としては、c.lr.N の設定に c.server.initialLimitedReaderSize() が使用されるようになったことです。これは、io.LimitedReader がリクエストヘッダーの読み取りに際して、サーバーが設定した最大ヘッダーサイズを超えないようにするためのものです。

  • (*conn).serve() メソッド: このメソッドは、個々のHTTPコネクションのライフサイクルを管理する主要なループです。 最も重要な変更は、StateActive への遷移ロジックです。

    if c.lr.N != c.server.initialLimitedReaderSize() {
        // If we read any bytes off the wire, we're active.
        c.setState(c.rwc, StateActive)
    }
    

    この条件文は、c.readRequest() が呼び出された後、io.LimitedReader (c.lr) が初期サイズ (c.server.initialLimitedReaderSize()) から減少しているかどうかをチェックします。

    • もし c.lr.N が初期サイズと異なる場合、それは実際にワイヤーから何らかのバイト(リクエストヘッダーの一部など)が読み取られたことを意味します。この場合にのみ、コネクションは StateActive に遷移します。
    • もし c.lr.N が初期サイズのままであれば、それはリクエストの読み取り中に何もバイトが読み取られなかったことを意味します(例: クライアントが接続後すぐに切断した、またはタイムアウトした)。この場合、コネクションは StateActive に遷移せず、StateNew または StateIdle のまま、最終的に StateClosed に遷移します。 この変更により、StateActive の定義がより厳密に適用され、コネクションが実際にアクティブなデータ転送を行っている場合にのみこの状態が報告されるようになります。

    また、c.hijacked() のチェック後の c.setState(origConn, StateHijacked) の行が削除されました。これは、hijack() メソッド内で既に StateHijacked が設定されるようになったため、冗長になったためです。

src/pkg/net/http/serve_test.go

  • TestServerConnState 関数: このテスト関数は、ConnState コールバックが様々なシナリオで期待通りに動作するかを検証します。
    • "/hijack-panic" ハンドラの追加とそれに対応する mustGet 呼び出しは、Hijack() 後にパニックが発生しても StateHijacked が適切に通知されることを確認するためのものです。
    • 新しいテストブロック(// New->Closed, // New->Active->Closed, // New->Idle->Closed)は、StateActive の新しい厳密な条件を検証するために追加されました。
      • New->Closed: クライアントが接続後すぐに切断した場合、StateActive を経由せずに StateClosed に直接遷移することを確認します。
      • New->Active->Closed: クライアントが不正なリクエストを送信し、サーバーがそれを読み取った後で切断した場合、StateActive を経由して StateClosed に遷移することを確認します。
      • New->Idle->Closed: クライアントが有効なリクエストを送信し、レスポンスを受け取った後、コネクションがアイドル状態になり、その後切断された場合、StateActiveStateIdle を経由して StateClosed に遷移することを確認します。 これらのテストは、StateActive の遷移が実際にワイヤーからのバイト読み取りに依存するようになったことを検証し、net/http サーバーのコネクション状態管理の正確性を保証します。

関連リンク

参考にした情報源リンク