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

[インデックス 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言語のpanicrecover

Go言語には、例外処理のメカニズムとしてpanicrecoverがあります。

  • panic: プログラムの実行を中断し、現在のゴルーチンをパニック状態にします。パニックが発生すると、現在の関数から呼び出し元の関数へとスタックがアンワインドされていき、その過程でdeferされた関数が実行されます。panicは任意の型の値を引数に取ることができ、その値がパニックの原因として伝播されます。
  • recover: panicが発生したゴルーチン内でdeferされた関数の中から呼び出すことで、パニックを捕捉し、そのゴルーチンのパニック状態を解除して通常の実行フローに戻すことができます。recoverは、捕捉したパニックの値を返します。recoverdeferされた関数以外から呼び出された場合、またはパニックが発生していないゴルーチンで呼び出された場合は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)が発生し、内部のrecovernilを返して再パニックしなかった場合でも、コネクションが適切に閉じられることが保証されます。

テストコードの変更も重要です。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言語のpanicrecoverに関する公式ドキュメントやブログ記事
  • net/httpパッケージのソースコード
  • Go言語のdeferステートメントに関する解説