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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける sendfile テストの競合状態(race condition)を修正するものです。具体的には、HTTPクライアントが最初のリクエストのレスポンスボディを完全に読み込む前に、次のリクエスト(/quit)を送信してしまうことで発生するテストの不安定性を解消します。

コミット

commit 9578839d60fb0d49130d6689091573aa390f85a0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Feb 16 09:27:26 2012 +1100

    net/http: fix race in sendfile test
    
    Whoops. Consume the body of the first request
    before making the subsequent /quit request.
    
    R=golang-dev, untheoretic
    CC=golang-dev
    https://golang.org/cl/5674054

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

https://github.com/golang/go/commit/9578839d60fb0d49130d6689091573aa390f85a0

元コミット内容

このコミットの元のメッセージは以下の通りです。

net/http: fix race in sendfile test

Whoops. Consume the body of the first request
before making the subsequent /quit request.

これは、net/http パッケージの sendfile テストにおける競合状態を修正するものであり、その原因が最初のHTTPリクエストのレスポンスボディを適切に消費する前に次のリクエストが発行されることにあると説明しています。

変更の背景

この変更は、net/http パッケージ内の TestLinuxSendfile というテストが、特定の条件下で不安定になる問題を解決するために行われました。不安定性の原因は、テスト内でHTTPリクエストが連続して行われる際に発生する競合状態にありました。

具体的には、テストはまずHTTPサーバーに対して最初のリクエストを送信し、その後、サーバープロセスを終了させるための /quit リクエストを送信します。問題は、最初のHTTPリクエストに対するレスポンスボディが完全に読み込まれる前に、/quit リクエストが送信されてしまう可能性があったことです。

HTTPプロトコルでは、クライアントはサーバーからのレスポンスボディを完全に読み込むか、接続を閉じる必要があります。もしレスポンスボディが完全に読み込まれないままクライアントが次のリクエストを同じ接続で送信しようとしたり、接続を閉じようとしたりすると、サーバー側で予期せぬ動作やエラーが発生する可能性があります。特に、サーバーがまだレスポンスボディの送信を完了していない場合、クライアントが接続を閉じると、サーバーは「broken pipe」などのエラーを受け取る可能性があります。

この競合状態は、テストが実行される環境のタイミングやリソースの利用状況によって顕在化したりしなかったりするため、テストが時々失敗する「flaky test」として現れていました。このような不安定なテストは、CI/CDパイプラインの信頼性を損ない、開発者が実際のバグとテストの不安定性を区別するのを困難にします。そのため、テストの信頼性を向上させるために、この競合状態を修正する必要がありました。

前提知識の解説

HTTPのレスポンスボディの消費

HTTP/1.1では、クライアントがサーバーからレスポンスを受け取った際、そのレスポンスにボディが含まれている場合(例: Content-Length ヘッダーがある、または Transfer-Encoding: chunked が指定されている場合)、クライアントはそのボディを完全に読み込む責任があります。これは、TCP接続の再利用(Keep-Alive)を適切に行うために重要です。

もしクライアントがレスポンスボディを完全に読み込まないまま次のリクエストを同じTCP接続で送信しようとすると、サーバーは前のレスポンスボディの残りを送信しようとし続けるため、プロトコル違反やデータ混同が発生する可能性があります。また、クライアントがボディを読み込まずに接続を閉じてしまうと、サーバーはまだ送信中のデータを破棄せざるを得なくなり、エラーログが出力されたり、リソースが適切に解放されなかったりする原因となります。

Go言語の net/http パッケージでは、http.Response オブジェクトの Body フィールドは io.ReadCloser インターフェースを実装しており、レスポンスボディを読み込むためのストリームとして機能します。このストリームは、ボディの読み込みが完了した後、またはエラーが発生した場合には必ず Close() メソッドを呼び出して閉じる必要があります。これにより、関連するリソース(特にTCP接続)が適切に解放され、接続の再利用が可能になります。

競合状態 (Race Condition)

競合状態とは、複数の並行に動作するプロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、HTTPクライアントが最初のレスポンスボディの読み込みと、次のリクエストの送信という2つの操作を並行して(または非常に短い間隔で)行おうとした際に発生しました。

具体的には、Get 関数がHTTPリクエストを送信し、http.Response を返しますが、この ResponseBody はまだ読み込まれていない状態です。この Body の読み込みが完了する前に、次の Get リクエスト(/quit)が発行されてしまうと、サーバー側で最初のレスポンスの送信が完了していないにもかかわらず、クライアントが新しいリクエストを送信しようとする、あるいは接続を閉じようとする、といった状況が発生し、テストが失敗する原因となっていました。

ioutil.Discardio.Copy

  • ioutil.Discard: io.Writer インターフェースを実装しており、書き込まれたデータをすべて破棄します。つまり、データをどこにも保存せずに読み捨てるための「ブラックホール」のようなものです。
  • io.Copy(dst io.Writer, src io.Reader): src からデータを読み込み、それを dst に書き込む関数です。src の終端に達するか、エラーが発生するまで読み書きを続けます。

このコミットでは、io.Copy(ioutil.Discard, res.Body) を使用して、res.Body からのデータをすべて読み込み、ioutil.Discard に書き込むことで、レスポンスボディを完全に消費しています。これにより、ボディの内容はメモリに保持されず、単に読み捨てられるため、リソース効率が良い方法でボディの読み込みを完了させることができます。

技術的詳細

このコミットの技術的な核心は、HTTPクライアントがサーバーからのレスポンスボディを完全に読み込むことの重要性にあります。元のコードでは、最初のHTTPリクエスト (Get(fmt.Sprintf("http://%s/", ln.Addr()))) の結果として返される http.Response オブジェクトの Body フィールドが適切に処理されていませんでした。

net/http パッケージの Get 関数は、HTTPリクエストを実行し、その結果として *http.Responseerror を返します。http.ResponseBody フィールドは io.ReadCloser 型であり、これはレスポンスボディのストリームを表します。このストリームは、サーバーがレスポンスボディの送信を完了するまで開いたままになります。

元のコードでは、Get の呼び出し後、res 変数にレスポンスが代入されていましたが、その res.Body からデータを読み込む処理がありませんでした。そのため、サーバーは最初のレスポンスボディの送信を継続しているにもかかわらず、クライアントはすぐに次のリクエスト (Get(fmt.Sprintf("http://%s/quit", ln.Addr()))) を送信しようとしていました。

この状況は、特にTCPのKeep-Aliveが有効な場合(HTTP/1.1のデフォルト動作)に問題を引き起こします。クライアントがレスポンスボディを完全に読み込まないまま次のリクエストを同じ接続で送信しようとすると、サーバーは前のレスポンスの残りを送信しようとし続けるため、プロトコルレベルでの同期が失われ、競合状態が発生します。結果として、テストが不安定になり、時折失敗する原因となっていました。

修正は、最初の Get リクエストの後に、io.Copy(ioutil.Discard, res.Body) を追加することで、この問題を解決しています。

  1. res, err := Get(...): 最初のHTTPリクエストを実行し、レスポンスを取得します。
  2. _, err = io.Copy(ioutil.Discard, res.Body): 取得したレスポンス res のボディ (res.Body) を ioutil.Discard にコピーします。ioutil.Discard は書き込まれたデータをすべて破棄するため、これはレスポンスボディの内容を読み捨てて、ストリームの終端まで読み進めることを意味します。これにより、サーバーが送信したレスポンスボディが完全にクライアントによって消費された状態になります。
  3. res.Body.Close(): レスポンスボディのストリームを閉じます。これは、io.ReadCloser の規約に従い、関連するリソース(TCP接続など)を適切に解放するために不可欠です。これにより、接続が再利用可能な状態になります。

これらの変更により、最初のHTTPリクエストのレスポンスボディが完全に処理されてから次の /quit リクエストが送信されることが保証され、テストにおける競合状態が解消されました。

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

変更は src/pkg/net/http/fs_test.go ファイルの TestLinuxSendfile 関数内で行われています。

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -398,11 +398,15 @@ func TestLinuxSendfile(t *testing.T) {
 		return
 	}
 
-	_, err = Get(fmt.Sprintf("http://%s/", ln.Addr()))
+	res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
 	if err != nil {
-		t.Errorf("http client error: %v", err)
-		return
+		t.Fatalf("http client error: %v", err)
 	}
+	_, err = io.Copy(ioutil.Discard, res.Body)
+	if err != nil {
+		t.Fatalf("client body read error: %v", err)
+	}
+	res.Body.Close()
 
 	// Force child to exit cleanly.
 	Get(fmt.Sprintf("http://%s/quit", ln.Addr()))

コアとなるコードの解説

変更前は、以下の行で最初のHTTPリクエストが送信されていました。

_, err = Get(fmt.Sprintf("http://%s/", ln.Addr()))

この行は Get 関数からの戻り値である *http.Response を破棄しており、レスポンスボディを読み込む処理が全くありませんでした。

変更後は、以下の3行が追加・修正されました。

  1. res, err := Get(fmt.Sprintf("http://%s/", ln.Addr())):

    • Get 関数からの *http.Response オブジェクトを res 変数に明示的に受け取るように変更されました。これにより、レスポンスボディ (res.Body) にアクセスできるようになります。
    • エラーハンドリングも t.Errorf から t.Fatalf に変更され、テストが致命的なエラーで即座に終了するように厳格化されています。
  2. _, err = io.Copy(ioutil.Discard, res.Body):

    • io.Copy 関数を使用して、res.Body からのデータを ioutil.Discard にコピーしています。
    • ioutil.Discard は書き込まれたデータをすべて破棄する io.Writer です。この操作により、res.Body ストリームの終端までデータが読み込まれ、レスポンスボディが完全に消費されます。
    • ここでもエラーハンドリングが追加され、ボディの読み込み中にエラーが発生した場合にテストが失敗するようにしています。
  3. res.Body.Close():

    • res.Bodyio.ReadCloser インターフェースを実装しているため、読み込みが完了した後には必ず Close() メソッドを呼び出す必要があります。
    • この呼び出しにより、HTTP接続に関連するリソースが適切に解放され、接続が再利用可能な状態になります。これは、特にHTTP Keep-Aliveが有効な場合に重要です。

これらの変更により、最初のHTTPリクエストのレスポンスボディが完全に読み込まれ、関連するリソースが解放されてから、次の /quit リクエストが送信されることが保証されます。これにより、テストの競合状態が解消され、テストの信頼性が向上しました。

関連リンク

  • Go言語の net/http パッケージに関する公式ドキュメント: https://pkg.go.dev/net/http
  • Go言語の io パッケージに関する公式ドキュメント: https://pkg.go.dev/io
  • Go言語の ioutil パッケージに関する公式ドキュメント (Go 1.16以降は io/ioutil は非推奨となり、io および os パッケージに機能が移行されていますが、このコミット時点では有効です): https://pkg.go.dev/io/ioutil

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージにある https://golang.org/cl/5674054 は、このGerritシステムへのリンクです)
  • HTTP/1.1 の仕様 (RFC 2616 - Section 8.1.2.2 Persistent Connections): https://www.rfc-editor.org/rfc/rfc2616#section-8.1.2.2
  • Go言語における io.Copyioutil.Discard の利用例に関する一般的な情報源 (例: ブログ記事、チュートリアルなど)
  • 競合状態に関する一般的なプログラミングの概念に関する情報源 (例: Wikipedia, プログラミング教本など)

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける sendfile テストの競合状態(race condition)を修正するものです。具体的には、HTTPクライアントが最初のリクエストのレスポンスボディを完全に読み込む前に、次のリクエスト(/quit)を送信してしまうことで発生するテストの不安定性を解消します。

コミット

commit 9578839d60fb0d49130d6689091573aa390f85a0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Feb 16 09:27:26 2012 +1100

    net/http: fix race in sendfile test
    
    Whoops. Consume the body of the first request
    before making the subsequent /quit request.
    
    R=golang-dev, untheoretic
    CC=golang-dev
    https://golang.org/cl/5674054

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

https://github.com/golang/go/commit/9578839d60fb0d49130d6689091573aa390f85a0

元コミット内容

このコミットの元のメッセージは以下の通りです。

net/http: fix race in sendfile test

Whoops. Consume the body of the first request
before making the subsequent /quit request.

これは、net/http パッケージの sendfile テストにおける競合状態を修正するものであり、その原因が最初のHTTPリクエストのレスポンスボディを適切に消費する前に次のリクエストが発行されることにあると説明しています。

変更の背景

この変更は、Go言語の標準ライブラリ net/http パッケージ内の TestLinuxSendfile というテストが、特定の条件下で不安定になる問題を解決するために行われました。不安定性の原因は、テスト内でHTTPリクエストが連続して行われる際に発生する競合状態にありました。

具体的には、テストはまずHTTPサーバーに対して最初のリクエストを送信し、その後、サーバープロセスを終了させるための /quit リクエストを送信します。問題は、最初のHTTPリクエストに対するレスポンスボディが完全に読み込まれる前に、/quit リクエストが送信されてしまう可能性があったことです。

HTTPプロトコルでは、クライアントはサーバーからのレスポンスボディを完全に読み込むか、接続を閉じる必要があります。もしレスポンスボディが完全に読み込まれないままクライアントが次のリクエストを同じ接続で送信しようとしたり、接続を閉じようとしたりすると、サーバー側で予期せぬ動作やエラーが発生する可能性があります。特に、サーバーがまだレスポンスボディの送信を完了していない場合、クライアントが接続を閉じると、サーバーは「broken pipe」などのエラーを受け取る可能性があります。

この競合状態は、テストが実行される環境のタイミングやリソースの利用状況によって顕在化したりしなかったりするため、テストが時々失敗する「flaky test」として現れていました。このような不安定なテストは、CI/CDパイプラインの信頼性を損ない、開発者が実際のバグとテストの不安定性を区別するのを困難にします。そのため、テストの信頼性を向上させるために、この競合状態を修正する必要がありました。

ウェブ検索の結果によると、net/http パッケージ自体には、sendfile に直接関連する既知の競合状態は広く報告されていませんが、一般的なHTTPハンドラにおける共有状態の不適切な同期や、トランスポート層での特定の条件下での競合状態は過去に報告されています。このコミットは、sendfile のテストという特定の文脈で、HTTPプロトコルの基本的な要件(レスポンスボディの完全な消費)が満たされていないことによる競合状態を修正したものです。

前提知識の解説

HTTPのレスポンスボディの消費

HTTP/1.1では、クライアントがサーバーからレスポンスを受け取った際、そのレスポンスにボディが含まれている場合(例: Content-Length ヘッダーがある、または Transfer-Encoding: chunked が指定されている場合)、クライアントはそのボディを完全に読み込む責任があります。これは、TCP接続の再利用(Keep-Alive)を適切に行うために重要です。

もしクライアントがレスポンスボディを完全に読み込まないまま次のリクエストを同じTCP接続で送信しようとすると、サーバーは前のレスポンスボディの残りを送信しようとし続けるため、プロトコル違反やデータ混同が発生する可能性があります。また、クライアントがボディを読み込まずに接続を閉じてしまうと、サーバーはまだ送信中のデータを破棄せざるを得なくなり、エラーログが出力されたり、リソースが適切に解放されなかったりする原因となります。

Go言語の net/http パッケージでは、http.Response オブジェクトの Body フィールドは io.ReadCloser インターフェースを実装しており、レスポンスボディを読み込むためのストリームとして機能します。このストリームは、ボディの読み込みが完了した後、またはエラーが発生した場合には必ず Close() メソッドを呼び出して閉じる必要があります。これにより、関連するリソース(特にTCP接続)が適切に解放され、接続の再利用が可能になります。

競合状態 (Race Condition)

競合状態とは、複数の並行に動作するプロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、HTTPクライアントが最初のレスポンスボディの読み込みと、次のリクエストの送信という2つの操作を並行して(または非常に短い間隔で)行おうとした際に発生しました。

具体的には、Get 関数がHTTPリクエストを送信し、http.Response を返しますが、この ResponseBody はまだ読み込まれていない状態です。この Body の読み込みが完了する前に、次の Get リクエスト(/quit)が発行されてしまうと、サーバー側で最初のレスポンスの送信が完了していないにもかかわらず、クライアントが新しいリクエストを送信しようとする、あるいは接続を閉じようとする、といった状況が発生し、テストが失敗する原因となっていました。

ioutil.Discardio.Copy

  • ioutil.Discard: io.Writer インターフェースを実装しており、書き込まれたデータをすべて破棄します。つまり、データをどこにも保存せずに読み捨てるための「ブラックホール」のようなものです。これは、読み込んだデータの内容自体には興味がなく、単にストリームを消費したい場合に非常に効率的です。
  • io.Copy(dst io.Writer, src io.Reader): src からデータを読み込み、それを dst に書き込む関数です。src の終端に達するか、エラーが発生するまで読み書きを続けます。

このコミットでは、io.Copy(ioutil.Discard, res.Body) を使用して、res.Body からのデータをすべて読み込み、ioutil.Discard に書き込むことで、レスポンスボディを完全に消費しています。これにより、ボディの内容はメモリに保持されず、単に読み捨てられるため、リソース効率が良い方法でボディの読み込みを完了させることができます。

技術的詳細

このコミットの技術的な核心は、HTTPクライアントがサーバーからのレスポンスボディを完全に読み込むことの重要性にあります。元のコードでは、最初のHTTPリクエスト (Get(fmt.Sprintf("http://%s/", ln.Addr()))) の結果として返される http.Response オブジェクトの Body フィールドが適切に処理されていませんでした。

net/http パッケージの Get 関数は、HTTPリクエストを実行し、その結果として *http.Responseerror を返します。http.ResponseBody フィールドは io.ReadCloser 型であり、これはレスポンスボディのストリームを表します。このストリームは、サーバーがレスポンスボディの送信を完了するまで開いたままになります。

元のコードでは、Get の呼び出し後、res 変数にレスポンスが代入されていましたが、その res.Body からデータを読み込む処理がありませんでした。そのため、サーバーは最初のレスポンスボディの送信を継続しているにもかかわらず、クライアントはすぐに次のリクエスト (Get(fmt.Sprintf("http://%s/quit", ln.Addr()))) を送信しようとしていました。

この状況は、特にTCPのKeep-Aliveが有効な場合(HTTP/1.1のデフォルト動作)に問題を引き起こします。クライアントがレスポンスボディを完全に読み込まないまま次のリクエストを同じ接続で送信しようとすると、サーバーは前のレスポンスの残りを送信しようとし続けるため、プロトコルレベルでの同期が失われ、競合状態が発生します。結果として、テストが不安定になり、時折失敗する原因となっていました。

修正は、最初の Get リクエストの後に、io.Copy(ioutil.Discard, res.Body) を追加することで、この問題を解決しています。

  1. res, err := Get(...): 最初のHTTPリクエストを実行し、レスポンスを取得します。
  2. _, err = io.Copy(ioutil.Discard, res.Body): 取得したレスポンス res のボディ (res.Body) を ioutil.Discard にコピーします。ioutil.Discard は書き込まれたデータをすべて破棄するため、これはレスポンスボディの内容を読み捨てて、ストリームの終端まで読み進めることを意味します。これにより、サーバーが送信したレスポンスボディが完全にクライアントによって消費された状態になります。
  3. res.Body.Close(): レスポンスボディのストリームを閉じます。これは、io.ReadCloser の規約に従い、関連するリソース(特にTCP接続)を適切に解放するために不可欠です。これにより、接続が再利用可能な状態になります。

これらの変更により、最初のHTTPリクエストのレスポンスボディが完全に処理されてから次の /quit リクエストが送信されることが保証され、テストにおける競合状態が解消されました。また、エラーハンドリングも t.Errorf から t.Fatalf に変更され、テストが致命的なエラーで即座に終了するように厳格化されています。これは、テストの信頼性をさらに高めるための良いプラクティスです。

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

変更は src/pkg/net/http/fs_test.go ファイルの TestLinuxSendfile 関数内で行われています。

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -398,11 +398,15 @@ func TestLinuxSendfile(t *testing.T) {
 		return
 	}
 
-	_, err = Get(fmt.Sprintf("http://%s/", ln.Addr()))
+	res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
 	if err != nil {
-		t.Errorf("http client error: %v", err)
-		return
+		t.Fatalf("http client error: %v", err)
 	}
+	_, err = io.Copy(ioutil.Discard, res.Body)
+	if err != nil {
+		t.Fatalf("client body read error: %v", err)
+	}
+	res.Body.Close()
 
 	// Force child to exit cleanly.
 	Get(fmt.Sprintf("http://%s/quit", ln.Addr()))

コアとなるコードの解説

変更前は、以下の行で最初のHTTPリクエストが送信されていました。

_, err = Get(fmt.Sprintf("http://%s/", ln.Addr()))

この行は Get 関数からの戻り値である *http.Response を破棄しており、レスポンスボディを読み込む処理が全くありませんでした。

変更後は、以下の3行が追加・修正されました。

  1. res, err := Get(fmt.Sprintf("http://%s/", ln.Addr())):

    • Get 関数からの *http.Response オブジェクトを res 変数に明示的に受け取るように変更されました。これにより、レスポンスボディ (res.Body) にアクセスできるようになります。
    • エラーハンドリングも t.Errorf から t.Fatalf に変更され、テストが致命的なエラーで即座に終了するように厳格化されています。t.Fatalf はエラーが発生した場合にテストを即座に終了させるため、後続の処理が不正な状態で行われることを防ぎます。
  2. _, err = io.Copy(ioutil.Discard, res.Body):

    • io.Copy 関数を使用して、res.Body からのデータを ioutil.Discard にコピーしています。
    • ioutil.Discard は書き込まれたデータをすべて破棄する io.Writer です。この操作により、res.Body ストリームの終端までデータが読み込まれ、レスポンスボディが完全に消費されます。これは、HTTPプロトコルにおいて、クライアントがレスポンスボディを完全に読み込むという重要な要件を満たすためのものです。
    • ここでもエラーハンドリングが追加され、ボディの読み込み中にエラーが発生した場合にテストが失敗するようにしています。
  3. res.Body.Close():

    • res.Bodyio.ReadCloser インターフェースを実装しているため、読み込みが完了した後には必ず Close() メソッドを呼び出す必要があります。
    • この呼び出しにより、HTTP接続に関連するリソース(特にTCP接続)が適切に解放され、接続が再利用可能な状態になります。これは、特にHTTP Keep-Aliveが有効な場合に、接続リークを防ぎ、効率的なリソース利用を促進するために不可欠です。

これらの変更により、最初のHTTPリクエストのレスポンスボディが完全に読み込まれ、関連するリソースが解放されてから、次の /quit リクエストが送信されることが保証されます。これにより、テストの競合状態が解消され、テストの信頼性が向上しました。

関連リンク

  • Go言語の net/http パッケージに関する公式ドキュメント: https://pkg.go.dev/net/http
  • Go言語の io パッケージに関する公式ドキュメント: https://pkg.go.dev/io
  • Go言語の ioutil パッケージに関する公式ドキュメント (Go 1.16以降は io/ioutil は非推奨となり、io および os パッケージに機能が移行されていますが、このコミット時点では有効です): https://pkg.go.dev/io/ioutil

参考にした情報源リンク