[インデックス 18726] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の ConnState
(コネクションの状態管理) の挙動を修正するものです。具体的には、StateHijacked
と StateActive
という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
StateHijacked
コールバックを早期に移動し、パニック耐性を持たせる。以前はHijack
の後にパニックが発生した場合、ConnState
がStateHijacked
を発火しなかった。これをhijack
時点に早期移動する。- リクエストを待っている間にワイヤーからバイトが読み取られない限り、
StateActive
を発火させない。これは、クライアントが切断またはタイムアウトした場合に、New
またはIdle
からActive
へ遷移しないことを意味する。これは以前から文書化されていたが、適切に実装されていなかった。
この変更は、Issue 7264 の保留中の修正に必要である。
変更の背景
このコミットは、Goの net/http
パッケージにおけるHTTPコネクションの状態管理 (ConnState
) の挙動に関する2つの主要な問題を解決するために行われました。
-
StateHijacked
の信頼性の向上:net/http
パッケージには、HTTPコネクションをHTTPサーバーの制御から「ハイジャック」し、低レベルのTCPコネクションを直接操作する機能があります。これはWebSocketのようなプロトコルを実装する際に利用されます。しかし、以前の実装では、コネクションがハイジャックされた直後にサーバー側でパニックが発生した場合、ConnState
のコールバックがStateHijacked
状態を適切に通知しないという問題がありました。これにより、コネクションの状態を正確に追跡しているアプリケーションで問題が発生する可能性がありました。このコミットは、StateHijacked
の通知をハイジャック処理のより早い段階に移動することで、このパニック耐性の問題を解決しようとしています。 -
StateActive
の厳密な適用:ConnState
のStateActive
は、コネクションがアクティブにリクエストを処理している状態を示すものです。net/http
のドキュメントでは、この状態は「ワイヤーからバイトが読み取られた場合」にのみ遷移するとされていました。しかし、実際の実装では、クライアントがリクエストを送信せずに切断したり、タイムアウトしたりした場合でも、不適切にStateActive
に遷移してしまうことがありました。これは、コネクションの状態管理の正確性を損ない、特にコネクションプーリングやリソース管理を行うアプリケーションにとって混乱を招く可能性がありました。このコミットは、StateActive
への遷移条件を厳密化し、実際にデータが読み取られた場合にのみ遷移するように修正することで、ドキュメントとの整合性を図っています。
これらの修正は、GoのHTTPサーバーの堅牢性と正確性を向上させることを目的としており、特に「Issue 7264」という特定のバグ修正に関連していることが明記されています。
前提知識の解説
このコミットの技術的詳細を理解するためには、以下のGo言語の net/http
パッケージに関する前提知識が必要です。
-
net/http
パッケージ: Go言語の標準ライブラリであり、HTTPクライアントとサーバーの実装を提供します。Webアプリケーション開発において中心的な役割を果たします。 -
http.Server
: HTTPサーバーを構成および実行するための構造体です。ListenAndServe
メソッドを通じてリクエストを待ち受け、処理します。 -
http.ConnState
:http.Server
の設定オプションの一つで、コネクションの状態変化を監視するためのコールバック関数を設定できます。このコールバックは、net.Conn
オブジェクトとhttp.ConnState
型の列挙値を受け取ります。http.ConnState
は以下の状態を定義します。http.StateNew
: 新しいコネクションが確立された直後の状態。http.StateActive
: コネクションがリクエストを読み取っている、またはレスポンスを書き込んでいる状態。http.StateIdle
: コネクションがアイドル状態であり、次のリクエストを待っている状態(Keep-Aliveコネクションの場合)。http.StateHijacked
: コネクションがHTTPサーバーの制御から「ハイジャック」され、アプリケーションが直接TCPコネクションを操作している状態。http.StateClosed
: コネクションが閉じられた状態。
-
http.Hijacker
インターフェース:http.ResponseWriter
が実装できるインターフェースで、Hijack()
メソッドを提供します。このメソッドを呼び出すことで、HTTPサーバーは基盤となるTCPコネクションの制御をアプリケーションに引き渡します。これにより、WebSocketのようなHTTP以外のプロトコルを同じポートで処理することが可能になります。Hijack()
が呼び出されると、net.Conn
とbufio.ReadWriter
が返され、アプリケーションはこれらを使って低レベルのネットワークI/Oを直接行います。 -
bufio.Reader
とbufio.Writer
: バッファリングされたI/Oを提供するGoの標準ライブラリです。ネットワークI/Oの効率を高めるためにnet/http
パッケージ内で広く利用されています。Hijack()
メソッドが*bufio.ReadWriter
を返すのは、HTTPリクエストの読み取り途中に残されたバッファ内のデータをアプリケーションが引き続き利用できるようにするためです。 -
パニック (Panic): Goにおけるランタイムエラーの一種です。パニックが発生すると、通常のプログラムフローは中断され、defer関数が実行された後、プログラムは終了します。HTTPサーバーのような長時間稼働するサービスでは、パニックは予期せぬシャットダウンを引き起こすため、適切に処理される必要があります。
-
io.LimitedReader
:io.Reader
をラップし、読み取ることができるバイト数を制限するリーダーです。net/http
サーバーでは、リクエストヘッダーの最大サイズを制限するために使用されます。これにより、悪意のある大きなヘッダーによるサービス拒否攻撃を防ぎます。
これらの概念を理解することで、コミットが ConnState
の挙動をどのように改善し、HTTPサーバーの堅牢性と正確性を高めているかを深く把握できます。
技術的詳細
このコミットは、net/http
パッケージにおけるコネクションの状態管理、特に StateHijacked
と StateActive
の遷移ロジックを修正することで、HTTPサーバーの信頼性と正確性を向上させています。
1. StateHijacked
の遷移タイミングの修正
問題点: 以前の実装では、http.ResponseWriter
の Hijack()
メソッドが呼び出された後、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
は「コネクションがリクエストを読み取っている、またはレスポンスを書き込んでいる状態」と定義されており、特に「ワイヤーからバイトが読み取られた場合」に遷移するとされていました。しかし、実際の実装では、クライアントがリクエストデータを送信する前に切断したり、タイムアウトしたりした場合でも、StateNew
や StateIdle
から不適切に 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.lr
はio.LimitedReader
であり、リクエストヘッダーの読み取りに使用されます。c.server.initialLimitedReaderSize()
は、io.LimitedReader
が初期状態で読み取れる最大バイト数(maxHeaderBytes
+bufio slop
)を返します。- この条件は、
c.readRequest()
が呼び出された後、io.LimitedReader
が初期サイズから減少している(つまり、実際にワイヤーから何らかのバイトが読み取られた)場合にのみStateActive
に遷移するようにします。- もし
c.lr.N
がinitialLimitedReaderSize()
と同じままであれば、それはリクエストヘッダーの読み取り中に何もバイトが読み取られなかったことを意味します(例: クライアントが接続後すぐに切断した、またはタイムアウトした)。この場合、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
-
(*conn).hijack()
メソッドの変更:// 変更前: // (StateHijacked の設定は serve() メソッド内で行われていた) // 変更後: func (c *conn) hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) { // ... (既存のコード) ... c.setState(rwc, StateHijacked) // ここに移動 return }
StateHijacked
の設定が、hijack()
メソッド内でコネクションがハイジャックされた直後に行われるようになりました。 -
(*Server).initialLimitedReaderSize()
メソッドの追加:// 追加: func (srv *Server) initialLimitedReaderSize() int64 { return int64(srv.maxHeaderBytes()) + 4096 // bufio slop }
io.LimitedReader
の初期サイズを計算するためのヘルパーメソッドが追加されました。 -
(*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()
メソッドが使用されるようになりました。 -
(*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
TestServerConnState
関数の変更:"/hijack-panic"
という新しいハンドラが追加されました。このハンドラはHijack()
を呼び出した後、意図的にパニックを発生させます。これはStateHijacked
のパニック耐性をテストするためです。ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0)
が追加され、テスト中のエラーログ出力を抑制しています。- 新しいテストケースが追加され、
StateNew -> StateClosed
、StateNew -> StateActive -> StateClosed
、StateNew -> 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
: クライアントが有効なリクエストを送信し、レスポンスを受け取った後、コネクションがアイドル状態になり、その後切断された場合、StateActive
とStateIdle
を経由してStateClosed
に遷移することを確認します。 これらのテストは、StateActive
の遷移が実際にワイヤーからのバイト読み取りに依存するようになったことを検証し、net/http
サーバーのコネクション状態管理の正確性を保証します。
関連リンク
- Go言語の
net/http
パッケージ公式ドキュメント: https://pkg.go.dev/net/http - Go言語の
ConnState
型に関するドキュメント: https://pkg.go.dev/net/http#ConnState - Go言語の
Hijacker
インターフェースに関するドキュメント: https://pkg.go.dev/net/http#Hijacker
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/69860049
は、このGerritシステムへのリンクです。) - Go言語のIssueトラッカー (GitHub Issues): https://github.com/golang/go/issues (Issue 7264 は公開されているIssueトラッカーでは直接見つかりませんでしたが、関連する問題はここで議論される可能性があります。)
- Go言語の公式ブログや設計ドキュメント (Goの設計思想や特定の機能に関する詳細な情報源)
io.LimitedReader
のドキュメント: https://pkg.go.dev/io#LimitedReaderbufio
パッケージのドキュメント: https://pkg.go.dev/bufio- Goにおけるパニックとリカバリに関する情報 (例:
defer
とrecover
): https://go.dev/blog/defer-panic-and-recover