[インデックス 14692] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージにおけるサーバー接続のリークバグを修正するものです。具体的には、HTTPハンドラがpanic(nil)
(nil
値でパニック)を起こした場合に、サーバーのコネクションが適切にクローズされずにリークしてしまう問題に対処しています。この修正により、panic(nil)
が発生した場合でもコネクションが確実に閉じられるようになり、リソースの枯渇を防ぎます。
コミット
commit 91934ff5d83807228d021925ed9d9d78d2b777e6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Dec 19 15:39:19 2012 -0800
net/http: fix server connection leak on Handler's panic(nil)
If a handler did a panic(nil), the connection was never closed.
Fixes #4050
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6971049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91934ff5d83807228d021925ed9d9d78d2b777e6
元コミット内容
HTTPハンドラがpanic(nil)
を実行した場合、サーバーコネクションが閉じられずにリークする問題を修正。
変更の背景
Go言語のnet/http
パッケージは、Webサーバーを構築するための基盤を提供します。このパッケージでは、クライアントからのリクエストを処理するためにハンドラ関数が使用されます。通常、ハンドラ関数内でエラーが発生した場合、適切なエラーハンドリングが行われるか、あるいはpanic
が発生した場合には、Goランタイムのメカニズムによってスタックがアンワインドされ、defer
された関数が実行されることでリソースが解放されます。
しかし、このコミットが修正する問題は、ハンドラがpanic(nil)
、つまりnil
値でパニックを起こした場合に特有のものでした。Goのpanic
は任意の型の値を引数に取ることができますが、nil
値でパニックした場合、net/http
サーバーの内部処理において、コネクションをクローズするためのdefer
された処理が適切に実行されないケースがありました。これにより、クライアントとの通信が終了しても、サーバー側のTCPコネクションが閉じられずに残り続け、結果としてコネクションリークが発生していました。
コネクションリークは、サーバーのリソース(ファイルディスクリプタ、メモリなど)を徐々に消費し尽くし、最終的には新しい接続を受け入れられなくなったり、サーバー全体のパフォーマンスが低下したりする深刻な問題を引き起こします。このバグはIssue #4050として報告されており、その解決がこのコミットの目的です。
前提知識の解説
Go言語のpanic
とrecover
Go言語には、例外処理のメカニズムとしてpanic
とrecover
があります。
panic
: プログラムの実行を中断し、現在のゴルーチンをパニック状態にします。パニックが発生すると、現在の関数から呼び出し元の関数へとスタックがアンワインドされていき、その過程でdefer
された関数が実行されます。panic
は任意の型の値を引数に取ることができ、その値がパニックの原因として伝播されます。recover
:panic
が発生したゴルーチン内でdefer
された関数の中から呼び出すことで、パニックを捕捉し、そのゴルーチンのパニック状態を解除して通常の実行フローに戻すことができます。recover
は、捕捉したパニックの値を返します。recover
がdefer
された関数以外から呼び出された場合、またはパニックが発生していないゴルーチンで呼び出された場合はnil
を返します。
このコミットの文脈では、panic(nil)
という特殊なケースが問題となります。panic
の引数にnil
が渡された場合、recover
で捕捉した際にnil
が返されるため、通常のパニック処理と区別がつきにくくなる可能性があります。
net/http
サーバーのコネクションハンドリング
net/http
パッケージのサーバーは、クライアントからの各TCP接続に対して新しいゴルーチンを起動し、そのゴルーチン内でHTTPリクエストの読み取り、ハンドラへのディスパッチ、レスポンスの書き込み、そしてコネクションのクローズといった一連の処理を行います。コネクションのクローズは通常、defer
ステートメントを使ってゴルーチンの終了時に確実に実行されるように設計されています。これにより、リクエスト処理中にエラーが発生したり、ハンドラがパニックを起こしたりしても、コネクションが適切に閉じられることが期待されます。
defer
ステートメント
Go言語のdefer
ステートメントは、そのステートメントを含む関数がリターンする直前(パニックによるスタックアンワインドを含む)に、指定された関数呼び出しをスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除、コネクションのクローズなど)を確実に行うために非常に有用です。
技術的詳細
この問題の根本原因は、net/http
パッケージのserver.go
内のconn.serve()
メソッドにありました。このメソッドは、個々のHTTPコネクションを処理するゴルーチンのエントリポイントです。以前の実装では、コネクションをクローズするc.close()
の呼び出しが、for
ループの後に直接配置されていました。
// Old code snippet (simplified)
func (c *conn) serve() {
// ...
for {
// ... request handling ...
// If a panic(nil) occurred here, and was recovered,
// the loop might break or continue, but c.close()
// might not be reached if the outer defer recover
// didn't re-panic or handle it correctly.
}
c.close() // This line might be skipped in certain panic(nil) scenarios
}
conn.serve()
の冒頭には、パニックを捕捉し、ログに記録するためのdefer
ブロックが存在します。しかし、panic(nil)
の場合、このdefer
ブロック内のrecover()
がnil
を返すため、パニックが「処理済み」と見なされ、再パニック(panic(err)
)が起こらない限り、ゴルーチンは正常終了したかのように振る舞うことがありました。この結果、for
ループが終了しても、c.close()
が呼び出されることなくゴルーチンが終了し、コネクションがリークするという状況が発生していました。
修正は、c.close()
の呼び出しをconn.serve()
メソッドの冒頭にdefer
ステートメントとして追加することで、この問題を解決しています。
// New code snippet (simplified)
func (c *conn) serve() {
defer func() {
if err := recover(); err != nil {
// ... log panic ...
// If err is nil (panic(nil)), this block might not re-panic.
}
c.rwc.Close() // This was the original defer, but it's inside the recover block.
}()
defer c.close() // <-- New line: ensures c.close() is always called
// ... rest of the serve logic ...
}
defer c.close()
をconn.serve()
の冒頭に追加することで、この関数がどのような形で終了しようとも(正常終了、パニック、return
ステートメントによる早期終了など)、c.close()
が確実に実行されるようになります。これにより、panic(nil)
が発生し、内部のrecover
がnil
を返して再パニックしなかった場合でも、コネクションが適切に閉じられることが保証されます。
テストコードの変更も重要です。TestHandlerPanicNil
という新しいテストケースが追加され、testHandlerPanic
ヘルパー関数がpanicValue interface{}
を受け入れるように変更されました。これにより、nil
値でのパニックをシミュレートし、コネクションが正しくクローズされることを検証できるようになりました。
コアとなるコードの変更箇所
src/pkg/net/http/serve_test.go
TestHandlerPanicNil
関数が追加されました。これはpanic(nil)
をテストするための新しいテストケースです。testHandlerPanic
関数のシグネチャが変更され、panicValue interface{}
という新しい引数が追加されました。これにより、テストで任意の値をパニックさせることができるようになりました。panic("intentional death for testing")
の代わりにpanic(panicValue)
が使用されるようになりました。- テストのログ出力に関するエラーハンドリングが
t.Fatal(err)
からt.Error(err)
に変更されました。これは、panic(nil)
の場合にエラーが期待されないため、テストが失敗するのではなく、単にエラーを報告するようにするためです。 panicValue == nil
の場合にテストを早期に終了させるロジックが追加されました。これは、panic(nil)
の場合には特定のログ出力が期待されないためです。
src/pkg/net/http/server.go
func (c *conn) serve()
メソッドの冒頭にdefer c.close()
が追加されました。func (c *conn) serve()
メソッドの末尾にあったc.close()
の呼び出しが削除されました。
コアとなるコードの解説
src/pkg/net/http/server.go
の変更
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -716,6 +716,7 @@ func (c *conn) serve() {
// ... (既存のdefer recoverとc.rwc.Close()のロジック) ...
}()
+ defer c.close() // <-- 追加された行
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
// ...
@@ -791,7 +792,6 @@ func (c *conn) serve() {
break
}
}
- c.close() // <-- 削除された行
}
この変更がこのコミットの核心です。
defer c.close()
の追加:conn.serve()
関数の冒頭にdefer c.close()
が追加されました。これにより、conn.serve()
関数がどのような理由(正常終了、return
ステートメント、またはpanic
によるスタックアンワインド)で終了するかにかかわらず、c.close()
メソッドが確実に呼び出されることが保証されます。これは、リソース(この場合はネットワークコネクション)の解放を確実に行うためのGoのイディオムです。- 末尾の
c.close()
の削除: 以前はfor
ループの後にc.close()
が直接呼び出されていましたが、defer
ステートメントによってコネクションのクローズが保証されるようになったため、この冗長な呼び出しは削除されました。これにより、コードがよりクリーンになり、意図が明確になります。
この変更により、ハンドラがpanic(nil)
を起こし、conn.serve()
内のrecover
ブロックがnil
を返して再パニックしなかった場合でも、defer c.close()
が最終的に実行され、コネクションリークが防止されます。
src/pkg/net/http/serve_test.go
の変更
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -918,15 +918,19 @@ func TestZeroLengthPostAndResponse(t *testing.T) {
}
}
+func TestHandlerPanicNil(t *testing.T) {
+ testHandlerPanic(t, false, nil)
+}
+
func TestHandlerPanic(t *testing.T) {
- testHandlerPanic(t, false)
+ testHandlerPanic(t, false, "intentional death for testing")
}
func TestHandlerPanicWithHijack(t *testing.T) {
- testHandlerPanic(t, true)
+ testHandlerPanic(t, true, "intentional death for testing")
}
-func testHandlerPanic(t *testing.T, withHijack bool) {
+func testHandlerPanic(t *testing.T, withHijack bool, panicValue interface{}) {
// ...
}))
defer ts.Close()
@@ -955,7 +959,7 @@ func testHandlerPanic(t *testing.T, withHijack bool) {
}
defer rwc.Close()
}
- panic("intentional death for testing")
+ panic(panicValue) // <-- panic(nil)をシミュレート可能に
}))
defer ts.Close()
@@ -968,7 +972,7 @@ func testHandlerPanic(t *testing.T, withHijack bool) {
_, err := pr.Read(buf)
pr.Close()
if err != nil {
- t.Fatal(err)
+ t.Error(err) // <-- t.Fatalからt.Errorに変更
}
done <- true
}()
@@ -978,6 +982,10 @@ func testHandlerPanic(t *testing.T, withHijack bool) {
t.Logf("expected an error")
}
+ if panicValue == nil { // <-- panic(nil)の場合の特殊処理
+ return
+ }
+
select {
case <-done:
return
TestHandlerPanicNil
の追加:panic(nil)
という特定のケースを検証するための専用テストが追加されました。これは、testHandlerPanic
ヘルパー関数をpanicValue
としてnil
を渡して呼び出します。testHandlerPanic
の汎用化:panicValue interface{}
引数を導入することで、testHandlerPanic
関数が文字列だけでなく、nil
を含む任意の値をパニックさせることができるようになりました。これにより、テストの柔軟性が向上し、さまざまなパニックシナリオを検証できるようになります。- エラーハンドリングの調整:
pr.Read(buf)
からのエラー処理がt.Fatal(err)
からt.Error(err)
に変更されました。これは、panic(nil)
の場合、pr.Read
がエラーを返すことが期待されないため、テストが即座に終了するのではなく、単にエラーを報告するようにするためです。 panicValue == nil
の特殊処理:panic(nil)
の場合、特定のログ出力が期待されないため、その場合はテストを早期に終了させるロジックが追加されました。これにより、テストがより正確にpanic(nil)
の挙動を検証できるようになります。
これらのテストの変更は、server.go
の修正がpanic(nil)
のシナリオで正しく機能することを保証するための重要なステップです。
関連リンク
- Go Issue #4050: https://github.com/golang/go/issues/4050
- Go CL 6971049: https://golang.org/cl/6971049
参考にした情報源リンク
- Go言語の
panic
とrecover
に関する公式ドキュメントやブログ記事 net/http
パッケージのソースコード- Go言語の
defer
ステートメントに関する解説