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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージ内のテストコード fs_test.go におけるデータ競合(data race)を修正するものです。具体的には、HTTPサーバーがまだレスポンスボディを送信している最中に、クライアント側がファイルディスクリプタ(fd)を閉じてしまうことによって発生する競合状態を解消します。

コミット

commit 600de1fb3db279be87f4b9fab0a09463fe1568e1
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sat Nov 3 00:26:36 2012 +0400

    net/http: fix data race in test
    The issue is that server still sends body,
    when client closes the fd.
    Fixes #4329.
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/6822072

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

https://github.com/golang/go/commit/600de1fb3db279be87f4b9fab0a09463fe1568e1

元コミット内容

net/http: fix data race in test The issue is that server still sends body, when client closes the fd. Fixes #4329.

変更の背景

このコミットは、net/http パッケージのテストコード fs_test.go において発生していたデータ競合を修正するために行われました。データ競合は、複数のゴルーチン(goroutine)が同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。このような競合は、プログラムの実行結果を予測不能にし、デバッグを困難にします。

この特定のケースでは、HTTPサーバーがクライアントに対してレスポンスボディを送信している最中に、クライアント側がソケットのファイルディスクリプタを閉じてしまうという状況で問題が発生していました。通常、クライアントが接続を閉じると、サーバーはそのシグナルを受け取り、送信を停止するべきです。しかし、テスト環境や特定のタイミングによっては、サーバーがまだデータを書き込もうとしている間にクライアントが接続を閉じてしまい、これにより競合状態が発生していました。これは、テストの不安定性(flakiness)を引き起こす可能性があり、テストが時々失敗する原因となります。

Fixes #4329 という記述は、このコミットが特定のバグトラッカーの課題番号4329を解決することを示しています。この課題は、おそらくGoプロジェクトの内部バグトラッカーで追跡されていたものと考えられます。

前提知識の解説

データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的な問題です。Go言語では、ゴルーチンという軽量な並行処理の仕組みを提供していますが、共有メモリにアクセスする際には注意が必要です。データ競合は以下の3つの条件がすべて満たされたときに発生します。

  1. 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Go言語には、データ競合を検出するためのツール(go run -race)が用意されています。

HTTP通信とソケットのクローズ

HTTP通信では、クライアントがリクエストを送信し、サーバーがレスポンスを返します。レスポンスにはヘッダーとボディが含まれることがあります。サーバーが大きなレスポンスボディを送信する場合、その送信は複数のパケットに分割されることがあります。

クライアントがHTTPリクエストを送信した後、レスポンスを受け取る前に接続を閉じることがあります。これは、例えばタイムアウト、ユーザーによるキャンセル、またはテストシナリオで意図的に行われる場合があります。このとき、サーバーがまだレスポンスボディの送信を完了していない場合、サーバーは閉じられたソケットに書き込もうとします。この「書き込み」と、クライアント側での「ソケットのクローズ」が同時に発生することで、データ競合が発生する可能性があります。

io.Copyioutil.Discard

  • io.Copy(dst, src): io.Reader インターフェースを実装する src から io.Writer インターフェースを実装する dst へデータをコピーする関数です。src から読み込みが終了するか、エラーが発生するまでコピーを続けます。
  • ioutil.Discard: io.Writer インターフェースを実装する特殊なオブジェクトです。このオブジェクトに書き込まれたデータはすべて破棄されます。つまり、データをどこにも保存せずに読み捨てるために使用されます。

res.Body.Close()

HTTPレスポンスのボディ (res.Body) は io.ReadCloser インターフェースを実装しており、これは io.Readerio.Closer の両方のインターフェースを兼ね備えています。Close() メソッドは、レスポンスボディに関連付けられたリソース(通常はネットワーク接続)を解放するために呼び出す必要があります。これを呼び出さないと、リソースリークが発生する可能性があります。

技術的詳細

このデータ競合は、テストコード内でHTTPリクエストを送信し、そのレスポンスを処理する際に発生していました。問題の根本原因は、クライアント側がレスポンスボディを完全に読み込む前に、またはサーバーがボディの送信を完了する前に、テストが次の処理に進んでしまい、結果的に接続が閉じられてしまうことにありました。

具体的には、テストコードがHTTPリクエストを送信し、レスポンスヘッダーを受け取った後、レスポンスボディの読み込みを適切に待たずに、すぐに次のテストアサーションやクリーンアップ処理に進んでしまう可能性がありました。この「読み込みの不完全さ」が、サーバーがまだボディを送信しようとしている間にクライアント側の接続が閉じられるという状況を生み出し、データ競合を引き起こしていました。

修正は、この問題を解決するために、レスポンスボディを明示的に完全に読み込み、その後で接続を閉じるというアプローチを取っています。

  1. io.Copy(ioutil.Discard, res.Body): この行は、サーバーから送信される可能性のある残りのレスポンスボディをすべて読み込み、ioutil.Discard に書き込むことで破棄します。これにより、クライアントはサーバーが送信を完了するまで待機し、ネットワークバッファに残っている可能性のあるデータをすべて消費します。これにより、サーバーが閉じられた接続に書き込もうとする状況を回避できます。
  2. res.Body.Close(): io.Copy が完了した後、またはエラーが発生した場合でも、res.Body.Close() を呼び出すことで、レスポンスボディに関連付けられたネットワークリソースが確実に解放されます。これは、リソースリークを防ぐための重要なステップです。

これらの変更により、テストが実行される際に、クライアントとサーバー間の通信がより適切に同期され、サーバーがまだデータを送信しようとしている間にクライアントが接続を閉じることによるデータ競合が解消されます。これにより、テストの信頼性が向上し、不安定なテストの失敗が減少します。

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

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

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -648,6 +648,8 @@ func TestServeContent(t *testing.T) {
 		if err != nil {
 			t.Fatal(err)
 		}
+		io.Copy(ioutil.Discard, res.Body)
+		res.Body.Close()
 		if res.StatusCode != tt.wantStatus {
 			t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
 		}

追加された行は以下の2行です。

  • io.Copy(ioutil.Discard, res.Body)
  • res.Body.Close()

これらの行は、既存の if err != nil { t.Fatal(err) } ブロックの直後、かつ if res.StatusCode != tt.wantStatus { ... } の前に挿入されています。

コアとなるコードの解説

追加された2行は、HTTPレスポンスボディの適切な処理を保証し、データ競合を回避するために不可欠です。

  1. io.Copy(ioutil.Discard, res.Body):

    • res.Body は、HTTPレスポンスのボディを表す io.Reader です。
    • ioutil.Discard は、書き込まれたデータをすべて破棄する io.Writer です。
    • この行は、res.Body から読み込めるすべてのデータを ioutil.Discard にコピーします。これにより、レスポンスボディが完全に読み込まれ、ネットワークバッファに残っている可能性のあるデータがすべて消費されます。これは、サーバーがまだデータを送信しようとしている間にクライアントが接続を閉じるという状況を防ぐために重要です。もしこの処理を行わないと、サーバーがまだデータを送信している途中でクライアントが接続を閉じてしまい、データ競合が発生する可能性があります。
  2. res.Body.Close():

    • res.Bodyio.ReadCloser インターフェースを実装しているため、Close() メソッドを持っています。
    • この行は、レスポンスボディに関連付けられたネットワーク接続やその他のリソースを明示的に閉じ、解放します。io.Copy が完了した後、またはエラーが発生した場合でも、この Close() の呼び出しは必須です。これを怠ると、リソースリークが発生し、テストの実行中にソケットが枯渇するなどの問題を引き起こす可能性があります。

これらの変更により、テストはHTTPレスポンスボディを完全に消費し、関連するリソースを適切に解放するようになります。これにより、サーバーとクライアント間の通信がより堅牢になり、データ競合が解消され、テストの信頼性と安定性が向上します。

関連リンク

参考にした情報源リンク