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

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

このコミットは、Go言語の標準ライブラリ net パッケージにおけるネットワーク接続の読み書きデッドライン(タイムアウト)機能に関する単体テストを追加するものです。これらのテストは、デッドラインの永続性、読み書きデッドライン間の非干渉性、デッドラインのリセット機能、および SetDeadline()SetReadDeadline() の両方で読み込みデッドラインを設定できることなどを検証します。

コミット

net: add unit tests for read/write deadlines
The tests verify that deadlines are "persistent",
read/write deadlines do not interfere, can be reset,
read deadline can be set with both SetDeadline()
and SetReadDeadline(), etc.

R=golang-dev, bradfitz, dave
CC=golang-dev
https://golang.org/cl/6850070

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

https://github.com/golang/go/commit/74fcf82dd9c551fb0e2c2f8c323bba5b91da60bf

元コミット内容

Go言語の net パッケージに、読み書きデッドライン(タイムアウト)に関する包括的な単体テストが追加されました。これらのテストは、デッドラインが一度設定されると永続的に適用されること、読み込みデッドラインと書き込みデッドラインが互いに影響しないこと、デッドラインを適切にリセットできること、そして読み込みデッドラインが SetDeadline()SetReadDeadline() の両方のメソッドで設定可能であることを検証します。

変更の背景

Go言語の net パッケージは、ネットワークプログラミングにおいて非常に重要な役割を果たします。特に、ネットワークI/O操作におけるタイムアウト(デッドライン)の挙動は、アプリケーションの応答性、リソース管理、および堅牢性に直接影響します。しかし、デッドラインの設定、リセット、および異なる種類のデッドライン(読み込み、書き込み、全体)間の相互作用は複雑であり、予期せぬ挙動を引き起こす可能性があります。

このコミットの背景には、これらのデッドライン機能が期待通りに動作することを保証するための、より堅牢なテストカバレッジの必要性がありました。特に、以下の点について明確な検証が求められていました。

  1. デッドラインの永続性: 一度設定されたデッドラインが、その操作が完了するか、明示的にリセットされるまで有効であること。
  2. 読み書きデッドラインの独立性: 読み込み操作のデッドラインが書き込み操作に影響を与えず、その逆も同様であること。これにより、アプリケーションは読み込みと書き込みのタイムアウトを独立して制御できます。
  3. デッドラインのリセット: 設定されたデッドラインを無効化し、ブロッキングI/O操作を無期限に待機させる機能が正しく動作すること。
  4. SetDeadline()SetReadDeadline() の挙動: SetDeadline() が読み書き両方のデッドラインを設定する一方で、SetReadDeadline() が読み込みデッドラインのみを設定する際の挙動の整合性。

これらのテストを追加することで、net パッケージのデッドライン機能の信頼性が向上し、開発者がネットワークアプリケーションを構築する際の予期せぬタイムアウト関連の問題を減らすことが期待されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語のネットワークプログラミングとタイムアウトに関する概念を理解しておく必要があります。

  1. Go言語の net パッケージ: Go言語の標準ライブラリである net パッケージは、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/Oプリミティブを提供します。これには、リスナーの作成、接続の確立、データの読み書きを行うための型や関数が含まれます。

  2. net.Conn インターフェース: net.Conn は、ネットワーク接続を表す基本的なインターフェースです。これには ReadWriteClose メソッドの他に、タイムアウトを設定するための以下のメソッドが含まれます。

    • SetDeadline(t time.Time) error: このメソッドは、接続の将来の読み込みおよび書き込み操作の両方に適用されるデッドラインを設定します。t は、操作がタイムアウトする絶対時刻(UTC)です。time.Time{} (Go 1.13以降では time.Time{}noDeadline と同義) を渡すと、デッドラインが解除され、操作は無期限にブロックされる可能性があります。

    • SetReadDeadline(t time.Time) error: このメソッドは、接続の将来の読み込み操作にのみ適用されるデッドラインを設定します。t は、読み込み操作がタイムアウトする絶対時刻です。

    • SetWriteDeadline(t time.Time) error: このメソッドは、接続の将来の書き込み操作にのみ適用されるデッドラインを設定します。t は、書き込み操作がタイムアウトする絶対時刻です。

  3. time.Timetime.Duration:

    • time.Time: 特定の時点を表すGoの型です。time.Now() で現在の時刻を取得できます。
    • time.Duration: 時間の長さを表す型です。time.Secondtime.Millisecond などの定数を使って期間を指定できます。例えば、time.Now().Add(5 * time.Second) は現在から5秒後の時刻を計算します。
  4. noDeadline: Go 1.13以降で導入された time.Time{} のエイリアスで、デッドラインを解除するために使用されます。デッドラインが設定されていない状態、つまり操作が無期限にブロックされる可能性がある状態を示します。このコミット時点(2012年)では time.Time{} が使われていますが、概念は同じです。

  5. タイムアウトエラー: ネットワーク操作がデッドラインに達すると、net.Error インターフェースを満たすエラーが返されます。このエラーは Timeout() メソッドを持ち、タイムアウトエラーであるかどうかを判定できます。テストでは isTimeout(err) ヘルパー関数がこれを行います。

  6. net.Listener インターフェース: net.Listener は、着信ネットワーク接続をリッスンするためのインターフェースです。これには Accept() メソッドが含まれ、新しい接続が利用可能になるまでブロックします。SetDeadline() メソッドも持ち、Accept() 操作のタイムアウトを設定できます。

これらの概念を理解することで、コミットが追加するテストケースが何を検証しようとしているのか、そしてなぜそれが重要なのかを深く把握することができます。

技術的詳細

このコミットで追加されたテストは、Go言語の net パッケージにおけるデッドライン機能の挙動を、様々なシナリオで厳密に検証しています。主な技術的ポイントは以下の通りです。

  1. デッドラインの絶対時刻指定: Goのデッドラインは、time.Time 型で絶対時刻として指定されます。これは、操作が開始されてからの相対的な時間ではなく、特定の時刻までに完了しなければならないことを意味します。例えば、time.Now().Add(-1 * time.Second) は、過去の時刻をデッドラインとして設定するため、操作は即座にタイムアウトします。これは、デッドラインが既に過ぎている場合の挙動をテストするのに非常に有効です。

  2. デッドラインの永続性: SetDeadlineSetReadDeadlineSetWriteDeadline で一度デッドラインが設定されると、そのデッドラインは明示的にリセットされるか、または新しいデッドラインが設定されるまで、その後の関連するI/O操作に適用され続けます。テストでは、デッドラインを設定した後、複数回操作を試行し、毎回タイムアウトエラーが発生することを確認することで、この永続性を検証しています。

  3. 読み書きデッドラインの独立性: SetReadDeadlineSetWriteDeadline は、それぞれ読み込みと書き込みの操作にのみ影響を与えます。SetDeadline は両方に影響を与えますが、個別のデッドラインが設定されている場合は、より具体的なデッドラインが優先されます。テストでは、例えば SetDeadline で全体デッドラインを設定した後、SetReadDeadline で読み込みデッドラインを過去に設定し、読み込み操作がタイムアウトする一方で、書き込み操作は影響を受けないことを検証しています。これにより、アプリケーションはI/Oの方向ごとに異なるタイムアウト戦略を適用できます。

  4. デッドラインのリセット (noDeadline): デッドラインを解除するには、SetDeadline(noDeadline) (または time.Time{}) を呼び出します。これにより、その後のI/O操作は無期限にブロックされる可能性があります。テストでは、タイムアウトが発生した後、デッドラインをリセットし、操作がブロック状態に戻ることを確認しています。これは、リソースの解放や、特定の条件下での無限待機が必要な場合に重要です。

  5. 非同期操作とデッドライン: テストでは、go func() { ... }() を使用してI/O操作をゴルーチンで実行し、time.Sleepselect ステートメントを組み合わせて、デッドラインが発動する前に操作がブロックされていることを確認しています。その後、接続をクローズすることで、ブロックされていた操作が errClosing エラーで終了することを確認しています。これは、デッドラインが正しく機能しているか、および接続クローズ時の挙動を検証する上で重要です。

  6. TestAcceptTimeout の検証: net.ListenerAccept() メソッドに対するデッドラインの挙動を検証します。リスナーにデッドラインを設定し、Accept() がタイムアウトエラーを返すことを確認します。また、デッドラインをリセットした後に Accept() がブロック状態に戻ることも検証します。

  7. TestReadTimeout の検証: net.ConnRead() メソッドに対するデッドラインの挙動を検証します。SetDeadlineSetReadDeadline の両方を使って読み込みデッドラインを設定し、Read() がタイムアウトエラーを返すことを確認します。また、書き込みデッドラインが設定されていても読み込みデッドラインに影響しないこと、およびデッドラインをリセットした後に Read() がブロック状態に戻ることも検証します。

  8. TestWriteTimeout の検証: net.ConnWrite() メソッドに対するデッドラインの挙動を検証します。SetDeadlineSetWriteDeadline の両方を使って書き込みデッドラインを設定し、Write() がタイムアウトエラーを返すことを確認します。特に、書き込みバッファが満杯になり、実際にネットワークI/Oがブロックされる状況をシミュレートするために、大きなバッファを繰り返し書き込む writeUntilTimeout ヘルパー関数を使用しています。

これらのテストは、Goの net パッケージのデッドライン機能が、様々なエッジケースや相互作用において期待通りに動作することを保証するための、包括的かつ詳細な検証を提供しています。

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

変更は src/pkg/net/timeout_test.go ファイルに集中しており、主に以下の3つの新しいテスト関数が追加されています。

  1. TestAcceptTimeout: net.ListenerAccept() メソッドのデッドライン挙動をテストします。
  2. TestReadTimeout: net.ConnRead() メソッドのデッドライン挙動をテストします。
  3. TestWriteTimeout: net.ConnWrite() メソッドのデッドライン挙動をテストします。

また、既存の TestDeadlineReset 関数内で、デッドラインのリセット方法が time.Time{} から noDeadline に変更されています。

--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -24,6 +24,151 @@ type copyRes struct {
  	d   time.Duration
  }
 
+func TestAcceptTimeout(t *testing.T) {
+	switch runtime.GOOS {
+	case "plan9":
+		t.Logf("skipping test on %q", runtime.GOOS)
+		return
+	}
+
+	ln := newLocalListener(t).(*TCPListener)
+	defer ln.Close()
+	ln.SetDeadline(time.Now().Add(-1 * time.Second))
+	if _, err := ln.Accept(); !isTimeout(err) {
+		t.Fatalf("Accept: expected err %v, got %v", errTimeout, err)
+	}
+	if _, err := ln.Accept(); !isTimeout(err) {
+		t.Fatalf("Accept: expected err %v, got %v", errTimeout, err)
+	}
+	ln.SetDeadline(time.Now().Add(100 * time.Millisecond))
+	if _, err := ln.Accept(); !isTimeout(err) {
+		t.Fatalf("Accept: expected err %v, got %v", errTimeout, err)
+	}
+	if _, err := ln.Accept(); !isTimeout(err) {
+		t.Fatalf("Accept: expected err %v, got %v", errTimeout, err)
+	}
+	ln.SetDeadline(noDeadline)
+	errc := make(chan error)
+	go func() {
+		_, err := ln.Accept()
+		errc <- err
+	}()
+	time.Sleep(100 * time.Millisecond)
+	select {
+	case err := <-errc:
+		t.Fatalf("Expected Accept() to not return, but it returned with %v\n", err)
+	default:
+	}
+	ln.Close()
+	if err := <-errc; err.(*OpError).Err != errClosing {
+		t.Fatalf("Accept: expected err %v, got %v", errClosing, err.(*OpError).Err)
+	}
+}
+
+func TestReadTimeout(t *testing.T) {
+	switch runtime.GOOS {
+	case "plan9":
+		t.Logf("skipping test on %q", runtime.GOOS)
+		return
+	}
+
+	ln := newLocalListener(t)
+	defer ln.Close()
+	c, err := DialTCP("tcp", nil, ln.Addr().(*TCPAddr))
+	if err != nil {
+		t.Fatalf("Connect: %v", err)
+	}
+	defer c.Close()
+	c.SetDeadline(time.Now().Add(time.Hour))
+	c.SetReadDeadline(time.Now().Add(-1 * time.Second))
+	buf := make([]byte, 1)
+	if _, err = c.Read(buf); !isTimeout(err) {
+		t.Fatalf("Read: expected err %v, got %v", errTimeout, err)
+	}
+	if _, err = c.Read(buf); !isTimeout(err) {
+		t.Fatalf("Read: expected err %v, got %v", errTimeout, err)
+	}
+	c.SetDeadline(time.Now().Add(100 * time.Millisecond))
+	if _, err = c.Read(buf); !isTimeout(err) {
+		t.Fatalf("Read: expected err %v, got %v", errTimeout, err)
+	}
+	if _, err = c.Read(buf); !isTimeout(err) {
+		t.Fatalf("Read: expected err %v, got %v", errTimeout, err)
+	}
+	c.SetReadDeadline(noDeadline)
+	c.SetWriteDeadline(time.Now().Add(-1 * time.Second))
+	errc := make(chan error)
+	go func() {
+		_, err := c.Read(buf)
+		errc <- err
+	}()
+	time.Sleep(100 * time.Millisecond)
+	select {
+	case err := <-errc:
+		t.Fatalf("Expected Read() to not return, but it returned with %v\n", err)
+	default:
+	}
+	c.Close()
+	if err := <-errc; err.(*OpError).Err != errClosing {
+		t.Fatalf("Read: expected err %v, got %v", errClosing, err.(*OpError).Err)
+	}
+}
+
+func TestWriteTimeout(t *testing.T) {
+	switch runtime.GOOS {
+	case "plan9":
+		t.Logf("skipping test on %q", runtime.GOOS)
+		return
+	}
+
+	ln := newLocalListener(t)
+	defer ln.Close()
+	c, err := DialTCP("tcp", nil, ln.Addr().(*TCPAddr))
+	if err != nil {
+		t.Fatalf("Connect: %v", err)
+	}
+	defer c.Close()
+	c.SetDeadline(time.Now().Add(time.Hour))
+	c.SetWriteDeadline(time.Now().Add(-1 * time.Second))
+	buf := make([]byte, 4096)
+	writeUntilTimeout := func() {
+		for {
+			_, err := c.Write(buf)
+			if err != nil {
+				if isTimeout(err) {
+					return
+				}
+				t.Fatalf("Write: expected err %v, got %v", errTimeout, err)
+			}
+		}
+	}
+	writeUntilTimeout()
+	c.SetDeadline(time.Now().Add(10 * time.Millisecond))
+	writeUntilTimeout()
+	writeUntilTimeout()
+	c.SetWriteDeadline(noDeadline)
+	c.SetReadDeadline(time.Now().Add(-1 * time.Second))
+	errc := make(chan error)
+	go func() {
+		for {
+			_, err := c.Write(buf)
+			if err != nil {
+				errc <- err
+			}
+		}
+	}()
+	time.Sleep(100 * time.Millisecond)
+	select {
+	case err := <-errc:
+		t.Fatalf("Expected Write() to not return, but it returned with %v\n", err)
+	default:
+	}
+	c.Close()
+	if err := <-errc; err.(*OpError).Err != errClosing {
+		t.Fatalf("Write: expected err %v, got %v", errClosing, err.(*OpError).Err)
+	}
+}
+
 func testTimeout(t *testing.T, net, addr string, readFrom bool) {
  	c, err := Dial(net, addr)
  	if err != nil {
@@ -117,7 +262,7 @@ func TestDeadlineReset(t *testing.T) {
  	defer ln.Close()
  	tl := ln.(*TCPListener)
  	tl.SetDeadline(time.Now().Add(1 * time.Minute))
--	tl.SetDeadline(time.Time{}) // reset it
-+	tl.SetDeadline(noDeadline) // reset it
  	errc := make(chan error, 1)
  	go func() {
  		_, err := ln.Accept()

コアとなるコードの解説

追加された各テスト関数は、Goの net パッケージにおけるデッドラインの挙動を、具体的なシナリオで検証しています。

TestAcceptTimeout

このテストは、net.ListenerAccept() メソッドに対するデッドラインの挙動を検証します。

  1. 即時タイムアウトの検証: ln.SetDeadline(time.Now().Add(-1 * time.Second)) を使用して、過去の時刻をデッドラインとして設定します。これにより、その後の ln.Accept() 呼び出しは即座にタイムアウトエラー (isTimeout(err)) を返すことが期待されます。これを複数回繰り返すことで、デッドラインが永続的であることを確認します。

  2. 短い期間のタイムアウト検証: ln.SetDeadline(time.Now().Add(100 * time.Millisecond)) で短い未来のデッドラインを設定し、同様に Accept() がタイムアウトエラーを返すことを確認します。

  3. デッドラインのリセットとブロッキング挙動の検証: ln.SetDeadline(noDeadline) を呼び出してデッドラインをリセットします。その後、ゴルーチン内で ln.Accept() を呼び出し、time.Sleep を使ってしばらく待機します。この間、Accept() はブロックされたままである(エラーを返さない)ことを select ステートメントで確認します。最後に、ln.Close() を呼び出すことで、ブロックされていた Accept()errClosing エラーで終了することを確認します。

TestReadTimeout

このテストは、net.ConnRead() メソッドに対するデッドラインの挙動を検証します。

  1. 初期設定: DialTCP で接続を確立し、c.SetDeadline(time.Now().Add(time.Hour)) で十分長い全体デッドラインを設定します。

  2. 即時読み込みタイムアウトの検証: c.SetReadDeadline(time.Now().Add(-1 * time.Second)) を使用して、過去の時刻を読み込みデッドラインとして設定します。これにより、その後の c.Read(buf) 呼び出しは即座にタイムアウトエラーを返すことが期待されます。

  3. 短い期間の読み込みタイムアウト検証: c.SetDeadline(time.Now().Add(100 * time.Millisecond)) で短い未来の全体デッドラインを設定し、c.Read(buf) がタイムアウトエラーを返すことを確認します。

  4. 読み込みデッドラインのリセットとブロッキング挙動の検証: c.SetReadDeadline(noDeadline) を呼び出して読み込みデッドラインをリセットします。さらに、c.SetWriteDeadline(time.Now().Add(-1 * time.Second)) で書き込みデッドラインを過去に設定し、読み込みデッドラインに影響がないことを確認します。その後、ゴルーチン内で c.Read(buf) を呼び出し、AcceptTimeout と同様に、Read() がブロックされたままであることを確認し、接続クローズ時に errClosing で終了することを確認します。

TestWriteTimeout

このテストは、net.ConnWrite() メソッドに対するデッドラインの挙動を検証します。

  1. 初期設定: DialTCP で接続を確立し、c.SetDeadline(time.Now().Add(time.Hour)) で十分長い全体デッドラインを設定します。

  2. 即時書き込みタイムアウトの検証: c.SetWriteDeadline(time.Now().Add(-1 * time.Second)) を使用して、過去の時刻を書き込みデッドラインとして設定します。writeUntilTimeout ヘルパー関数は、Write() がタイムアウトエラーを返すまで繰り返し書き込みを試行します。これにより、即座にタイムアウトが発生することを確認します。

  3. 短い期間の書き込みタイムアウト検証: c.SetDeadline(time.Now().Add(10 * time.Millisecond)) で短い未来の全体デッドラインを設定し、writeUntilTimeout を再度呼び出して、Write() がタイムアウトエラーを返すことを確認します。

  4. 書き込みデッドラインのリセットとブロッキング挙動の検証: c.SetWriteDeadline(noDeadline) を呼び出して書き込みデッドラインをリセットします。さらに、c.SetReadDeadline(time.Now().Add(-1 * time.Second)) で読み込みデッドラインを過去に設定し、書き込みデッドラインに影響がないことを確認します。その後、ゴルーチン内で c.Write(buf) を呼び出し、ReadTimeout と同様に、Write() がブロックされたままであることを確認し、接続クローズ時に errClosing で終了することを確認します。

TestDeadlineReset の変更

既存の TestDeadlineReset 関数では、デッドラインのリセットに tl.SetDeadline(time.Time{}) が使われていましたが、このコミットで tl.SetDeadline(noDeadline) に変更されました。これは機能的には同じですが、noDeadline を使用することで、コードの意図がより明確になります。

これらのテストは、Goのネットワークデッドライン機能の堅牢性と正確性を保証するために不可欠なものです。

関連リンク

参考にした情報源リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/74fcf82dd9c551fb0e2c2f8c323bba5b91da60bf
  • Go言語 net パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/net
  • Go言語 time パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/time
  • Go言語のネットワークプログラミングに関する一般的な情報源 (例: Go by Example - Network connections): https://gobyexample.com/network-connections (これは一般的な参考情報であり、特定のコミット内容に直接関連するものではありませんが、背景知識として有用です。)
  • Go言語のタイムアウトに関する一般的な情報源 (例: Go by Example - Timeouts): https://gobyexample.com/timeouts (これも一般的な参考情報であり、特定のコミット内容に直接関連するものではありませんが、背景知識として有用です。)
  • Go言語の noDeadline について (Go 1.13 Release Notes): https://go.dev/doc/go1.13#time (このコミットはGo 1.13より前ですが、noDeadline の概念を理解するのに役立ちます。)