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

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

このコミットでは、Go言語の標準ライブラリ net/http/cgi パッケージ内の2つのファイルが変更されています。

  • src/pkg/net/http/cgi/host.go: CGIハンドラの主要なロジックが含まれるファイル。子CGIプロセスとのI/O処理が定義されています。
  • src/pkg/net/http/cgi/matryoshka_test.go: net/http/cgi パッケージのテストファイル。特に、CGIプロセスが自身をホストするような入れ子構造のテストや、今回の変更に関連する新しいテストケースが追加されています。

コミット

commit 5db255fa3c8b3f5d5aa1560d1e5be4688dfb7925
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 6 11:24:28 2014 -0800

    net/http/cgi: kill child CGI process on copy error

    Fixes #7196

    LGTM=rsc
    R=golang-codereviews, rsc
    CC=golang-codereviews, iant
    https://golang.org/cl/69970052

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

https://github.com/golang/go/commit/5db255fa3c8b3f5d5aa1560d1e5be4688dfb7925

元コミット内容

net/http/cgi: コピーエラー時に子CGIプロセスを終了させる

Issue #7196 を修正します。

変更の背景

このコミットは、Goの net/http/cgi パッケージにおいて、CGIスクリプトの出力(標準出力)をHTTPレスポンスライターにコピーする際にエラーが発生した場合に、子CGIプロセスが終了せずにハングアップしてしまう問題を解決するために導入されました。

具体的には、io.Copy 関数がCGIプロセスの出力をクライアント(HTTPレスポンスライター)に書き込んでいる途中で、クライアント側が接続を切断するなどの理由で書き込みエラー(例えば broken pipe エラー)が発生した場合、CGIプロセスは自身の出力をどこにも書き込めなくなり、無限ループに陥ったり、単に終了せずにリソースを消費し続けたりする可能性がありました。これにより、サーバー側のリソースが枯渇したり、リクエストがタイムアウトするまで応答が返ってこないといった問題が発生していました。

Issue #7196 は、このハングアップ問題を報告していたものと考えられます。このコミットは、このような状況下でCGIプロセスを明示的に終了させることで、リソースの解放とシステムの安定性を確保することを目的としています。

前提知識の解説

CGI (Common Gateway Interface)

CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携して動的なコンテンツを生成するための標準的なインターフェースです。WebサーバーはHTTPリクエストを受け取ると、CGIスクリプトをプロセスとして起動し、環境変数を通じてリクエスト情報(HTTPヘッダー、クエリパラメータなど)を渡し、標準入力からリクエストボディを渡します。CGIスクリプトは処理結果を標準出力に書き出し、Webサーバーはその出力をHTTPレスポンスとしてクライアントに返します。

Goの net/http/cgi パッケージ

Go言語の net/http/cgi パッケージは、Goで書かれたHTTPサーバーがCGIスクリプトをホストするための機能を提供します。これにより、GoのHTTPサーバーが既存のCGIアプリケーションを実行したり、Goで書かれたプログラムをCGIスクリプトとして動作させたりすることが可能になります。このパッケージは、CGIプロセスの起動、環境変数の設定、標準入出力のリダイレクト、そしてプロセスの終了処理などを抽象化します。

io.Copy 関数

io.Copy(dst io.Writer, src io.Reader) は、Go言語の io パッケージで提供されるユーティリティ関数です。src からデータを読み込み、それを dst に書き込みます。src のEOF(End Of File)に達するか、コピー中にエラーが発生するまで処理を続けます。この関数は、ストリーム間のデータ転送で非常に頻繁に使用されます。

プロセス管理 (os.Process, cmd.Wait, cmd.Process.Kill)

  • os.Process: Go言語で実行中のプロセスを表す構造体です。プロセスのPID(プロセスID)などの情報を含みます。
  • cmd.Wait(): os/exec パッケージの Cmd 型のメソッドで、コマンドの実行が完了するまで待機し、その終了ステータスを返します。子プロセスが終了するまで親プロセスをブロックします。
  • cmd.Process.Kill(): os.Process 型のメソッドで、プロセスに終了シグナル(Unix系OSでは SIGKILL)を送信し、強制的に終了させます。これにより、プロセスは即座に停止し、クリーンアップ処理は行われません。

技術的詳細

このコミットの核心は、net/http/cgi パッケージの Handler 型の ServeHTTP メソッドにおける io.Copy の挙動の改善です。

問題点: 以前の実装では、CGIプロセスの標準出力からHTTPレスポンスライターへのデータコピー中にエラーが発生した場合、io.Copy はエラーを返して終了しますが、子CGIプロセス自体は実行を継続していました。特に、クライアントが接続を閉じた場合(例えば、ブラウザがページを途中で閉じたり、ダウンロードをキャンセルしたりした場合)、レスポンスライターへの書き込みが失敗し、io.Copy はエラーを返します。しかし、CGIプロセスはクライアントが切断されたことを知らず、自身の標準出力にデータを書き込み続けようとします。これにより、CGIプロセスはハングアップ状態になり、defer cmd.Wait() が永遠に完了しない可能性がありました。

解決策: このコミットでは、io.Copy がエラーを返した場合に、直ちに子CGIプロセスを cmd.Process.Kill() を使って強制終了させるロジックが追加されました。

  1. io.Copy のエラーハンドリングの強化: io.Copy(rw, linebody) の呼び出し後、返された errnil でない場合、つまりコピー中にエラーが発生した場合に、追加の処理が実行されます。
  2. 子プロセスの強制終了: エラーが発生した場合、cmd.Process.Kill() が呼び出され、子CGIプロセスに終了シグナルが送られます。これにより、CGIプロセスは即座に終了し、ハングアップ状態を回避できます。
  3. defer cmd.Wait() との連携: cmd.Process.Kill() が呼び出されることで、子プロセスは終了します。その結果、ServeHTTP メソッドの冒頭で defer cmd.Wait() によって登録された待機処理が、子プロセスが実際に終了した後に正常に完了するようになります。コメントにもあるように、もしエラーが子プロセス自身の終了による読み取りエラーであった場合でも、既に終了しているプロセスを再度 Kill しても無害です。PIDが再利用されるのは Wait が完了した後であるため、誤って別のプロセスを終了させる心配もありません。
  4. テストフックの追加: testHookStartProcess というテスト用のフックが追加されました。これは、CGIプロセスが起動された直後にその os.Process オブジェクトを受け取る関数ポインタです。このフックは通常 nil ですが、テスト時のみ設定され、テストコードが起動されたCGIプロセスを監視したり、必要に応じて操作したりすることを可能にします。これにより、TestKillChildAfterCopyError のような、特定の条件下で子プロセスが終了するかどうかを検証するテストが容易になります。

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

src/pkg/net/http/cgi/host.go

--- a/src/pkg/net/http/cgi/host.go
+++ b/src/pkg/net/http/cgi/host.go
@@ -214,6 +214,9 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		internalError(err)
 		return
 	}\n+\tif hook := testHookStartProcess; hook != nil {\n+\t\thook(cmd.Process)\n+\t}\n 	defer cmd.Wait()\n 	defer stdoutRead.Close()\
 \n@@ -292,6 +295,13 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\
 	_, err = io.Copy(rw, linebody)\
 	if err != nil {\
 		h.printf("cgi: copy error: %v", err)\
+\t\t// And kill the child CGI process so we don't hang on\
+\t\t// the deferred cmd.Wait above if the error was just\
+\t\t// the client (rw) going away. If it was a read error\
+\t\t// (because the child died itself), then the extra\
+\t\t// kill of an already-dead process is harmless (the PID\
+\t\t// won't be reused until the Wait above).\
+\t\tcmd.Process.Kill()\
 	}\
 }\
 \n@@ -348,3 +358,5 @@ func upperCaseAndUnderscore(r rune) rune {\
 	// TODO: other transformations in spec or practice?\
 	return r\
 }\
+\n+var testHookStartProcess func(*os.Process) // nil except for some tests

src/pkg/net/http/cgi/matryoshka_test.go

このファイルには、TestKillChildAfterCopyError という新しいテスト関数が追加され、customWriterRecorderlimitWriterneverEnding といったヘルパー型が定義されています。また、TestBeChildCGIProcesswrite-forever クエリパラメータのハンドリングが追加されています。

コアとなるコードの解説

host.go の変更点

  1. testHookStartProcess の追加と利用:

    +var testHookStartProcess func(*os.Process) // nil except for some tests
    // ...
    +	if hook := testHookStartProcess; hook != nil {
    +		hook(cmd.Process)
    +	}
    

    testHookStartProcess は、*os.Process を引数にとる関数型の変数です。デフォルトでは nil ですが、テスト時にのみ設定されます。CGIプロセスが起動され、その cmd.Process オブジェクトが利用可能になった直後に、このフックが設定されていれば呼び出されます。これにより、テストコードは起動された子プロセスの参照を取得し、その状態を監視したり、必要に応じて操作したりできるようになります。

  2. io.Copy エラー時の cmd.Process.Kill():

    	_, err = io.Copy(rw, linebody)
    	if err != nil {
    		h.printf("cgi: copy error: %v", err)
    +		// And kill the child CGI process so we don't hang on
    +		// the deferred cmd.Wait above if the error was just
    +		// the client (rw) going away. If it was a read error
    +		// (because the child died itself), then the extra
    +		// kill of an already-dead process is harmless (the PID
    +		// won't be reused until the Wait above).
    +		cmd.Process.Kill()
    	}
    

    これがこのコミットの主要な変更点です。io.Copy がエラーを返した場合、cmd.Process.Kill() が呼び出され、子CGIプロセスが強制的に終了されます。これにより、クライアント側の切断などによって io.Copy が失敗しても、CGIプロセスがバックグラウンドでハングアップし続けることを防ぎ、リソースリークやサーバーの応答性低下を防ぎます。コメントにあるように、子プロセスが既に終了している場合でも Kill() は安全です。

matryoshka_test.go の変更点

  1. customWriterRecorderlimitWriter: これらの型は、io.Copy のエラーをシミュレートするために導入されました。

    • customWriterRecorder: httptest.ResponseRecorder をラップし、内部の io.Writer をカスタマイズできるようにします。
    • limitWriter: 特定のバイト数までしか書き込みを許可しない io.Writer の実装です。指定されたバイト数を超えて書き込もうとするとエラーを返します。
  2. TestKillChildAfterCopyError 関数: この新しいテストは、io.Copy エラー時に子CGIプロセスが正しく終了するかどうかを検証します。

    • testHookStartProcess を設定し、起動されるCGIプロセスの *os.Process を取得します。
    • limitWriter を使用して、CGIプロセスの出力が一定量を超えると書き込みエラーが発生するように http.ResponseWriter を設定します。
    • CGIプロセスとして、write-forever=1 クエリパラメータを持つ自身(TestBeChildCGIProcess)を起動します。このCGIプロセスは、無限に 'a' を出力し続けようとします。
    • ServeHTTP をゴルーチンで実行し、タイムアウトを監視します。
    • 期待される動作は、limitWriter がエラーを返した後、ServeHTTPcmd.Process.Kill() を呼び出し、CGIプロセスが終了し、ServeHTTP が正常に完了することです。もし ServeHTTP がタイムアウトした場合、それは子プロセスがハングアップしていることを意味し、テストは失敗します。
  3. TestBeChildCGIProcess の変更:

    +		if req.FormValue("write-forever") == "1" {
    +			io.Copy(rw, neverEnding('a'))
    +			for {
    +				time.Sleep(5 * time.Second) // hang forever, until killed
    +			}
    +		}
    

    この変更により、テスト用のCGIプロセスが write-forever=1 クエリパラメータを受け取った場合、neverEnding('a') から無限に 'a' を http.ResponseWriter にコピーしようとします。これは、親プロセス側で io.Copy がエラーを発生させたときに、子プロセスがハングアップする状況をシミュレートするために使用されます。子プロセスは、親によって Kill されるまで無限ループで待機します。

これらの変更により、net/http/cgi パッケージは、CGIプロセスとのI/Oエラー発生時により堅牢になり、リソースの適切な解放とシステムの安定性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (net/http/cgi, io, os/exec パッケージ)
  • CGIの一般的な概念に関する情報
  • コミットメッセージとコードの差分
  • Go issue tracker (Issue #7196 の詳細については、公開されている情報が限られているため、コミットメッセージとコードから推測しました。)