[インデックス 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()
を使って強制終了させるロジックが追加されました。
io.Copy
のエラーハンドリングの強化:io.Copy(rw, linebody)
の呼び出し後、返されたerr
がnil
でない場合、つまりコピー中にエラーが発生した場合に、追加の処理が実行されます。- 子プロセスの強制終了: エラーが発生した場合、
cmd.Process.Kill()
が呼び出され、子CGIプロセスに終了シグナルが送られます。これにより、CGIプロセスは即座に終了し、ハングアップ状態を回避できます。 defer cmd.Wait()
との連携:cmd.Process.Kill()
が呼び出されることで、子プロセスは終了します。その結果、ServeHTTP
メソッドの冒頭でdefer cmd.Wait()
によって登録された待機処理が、子プロセスが実際に終了した後に正常に完了するようになります。コメントにもあるように、もしエラーが子プロセス自身の終了による読み取りエラーであった場合でも、既に終了しているプロセスを再度Kill
しても無害です。PIDが再利用されるのはWait
が完了した後であるため、誤って別のプロセスを終了させる心配もありません。- テストフックの追加:
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
という新しいテスト関数が追加され、customWriterRecorder
、limitWriter
、neverEnding
といったヘルパー型が定義されています。また、TestBeChildCGIProcess
に write-forever
クエリパラメータのハンドリングが追加されています。
コアとなるコードの解説
host.go
の変更点
-
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
オブジェクトが利用可能になった直後に、このフックが設定されていれば呼び出されます。これにより、テストコードは起動された子プロセスの参照を取得し、その状態を監視したり、必要に応じて操作したりできるようになります。 -
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
の変更点
-
customWriterRecorder
とlimitWriter
: これらの型は、io.Copy
のエラーをシミュレートするために導入されました。customWriterRecorder
:httptest.ResponseRecorder
をラップし、内部のio.Writer
をカスタマイズできるようにします。limitWriter
: 特定のバイト数までしか書き込みを許可しないio.Writer
の実装です。指定されたバイト数を超えて書き込もうとするとエラーを返します。
-
TestKillChildAfterCopyError
関数: この新しいテストは、io.Copy
エラー時に子CGIプロセスが正しく終了するかどうかを検証します。testHookStartProcess
を設定し、起動されるCGIプロセスの*os.Process
を取得します。limitWriter
を使用して、CGIプロセスの出力が一定量を超えると書き込みエラーが発生するようにhttp.ResponseWriter
を設定します。- CGIプロセスとして、
write-forever=1
クエリパラメータを持つ自身(TestBeChildCGIProcess
)を起動します。このCGIプロセスは、無限に 'a' を出力し続けようとします。 ServeHTTP
をゴルーチンで実行し、タイムアウトを監視します。- 期待される動作は、
limitWriter
がエラーを返した後、ServeHTTP
がcmd.Process.Kill()
を呼び出し、CGIプロセスが終了し、ServeHTTP
が正常に完了することです。もしServeHTTP
がタイムアウトした場合、それは子プロセスがハングアップしていることを意味し、テストは失敗します。
-
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 CL 69970052: https://golang.org/cl/69970052
参考にした情報源リンク
- Go言語の公式ドキュメント (
net/http/cgi
,io
,os/exec
パッケージ) - CGIの一般的な概念に関する情報
- コミットメッセージとコードの差分
- Go issue tracker (Issue #7196 の詳細については、公開されている情報が限られているため、コミットメッセージとコードから推測しました。)