[インデックス 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