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

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

このコミットは、Go言語の標準ライブラリnetパッケージにおいて、Windows環境でのAccept操作における読み込みデッドライン(read deadline)の適用に関する修正と、それに関連するテストの追加を行っています。具体的には、net.ListenerSetDeadlineメソッドがWindowsのAccept呼び出しで正しく機能しない問題を解決し、ネットワーク接続のタイムアウト処理を改善します。

コミット

commit d12a7d39d1cfa7a0cd824673d7aacc4615b461cb
Author: Alexey Borzenkov <snaury@gmail.com>
Date:   Wed Oct 31 09:58:05 2012 +1100

    net: use read deadline in Accept on windows
    
    Fixes #4296.
    
    R=golang-dev, alex.brainman
    CC=golang-dev
    https://golang.org/cl/6815044

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

https://github.com/golang/go/commit/d12a7d39d1cfa7a0cd824673d7aacc4615b461cb

元コミット内容

net: use read deadline in Accept on windows Fixes #4296.

このコミットは、Windows上でのnet.Accept呼び出しにおいて、読み込みデッドラインを使用するように変更します。これにより、Issue #4296で報告された問題が修正されます。

変更の背景

この変更の背景には、Go言語のnetパッケージにおけるnet.ListenerAcceptメソッドが、Windows環境でSetDeadlineによって設定されたタイムアウトを尊重しないという問題がありました。具体的には、net.Listenerに対してSetDeadlineを設定しても、Accept呼び出しが指定されたデッドラインを超えてブロックし続ける可能性がありました。これは、サーバーアプリケーションがクライアントからの接続を待機する際に、特定の時間内に接続が確立されない場合に処理を中断したり、リソースを解放したりするようなシナリオで問題となります。

Issue #4296("net: Accept on Windows ignores SetDeadline")では、この挙動が報告されており、WindowsのネットワークI/OモデルとGoのデッドライン処理の間の不整合が示唆されていました。Goのnetパッケージは、クロスプラットフォームで一貫したネットワークI/Oを提供することを目指していますが、OS固有のI/Oメカニズム(特にWindowsのCompletion Portsなど)との統合において、デッドラインの適用が課題となることがありました。このコミットは、この特定のWindows環境でのAcceptのデッドライン問題を解決することを目的としています。

前提知識の解説

  • net.ListenerAccept: Go言語のnetパッケージにおけるnet.Listenerインターフェースは、着信ネットワーク接続をリッスンするための一般的なインターフェースです。その主要なメソッドの一つであるAccept()は、次の着信接続をブロックして待機し、接続が確立されると新しいnet.Connインターフェースを返します。
  • デッドライン(Deadlines): Goのnetパッケージでは、SetDeadlineSetReadDeadlineSetWriteDeadlineといったメソッドを通じて、ネットワーク操作にタイムアウトを設定できます。これにより、特定の時間内に操作が完了しない場合にエラーを返すように設定でき、アプリケーションが無限にブロックされるのを防ぎます。
  • WindowsのネットワークI/O: Windowsでは、ネットワークI/Oは通常、Winsock APIを通じて行われます。非同期I/Oには、I/O Completion Ports (IOCP) などのメカニズムが使用され、効率的な並行処理を可能にします。Goのnetパッケージは、これらのOS固有のメカニズムを抽象化し、Goのgoroutineモデルと統合しています。
  • syscall.Sockaddr: syscallパッケージは、OSのシステムコールへの低レベルなインターフェースを提供します。syscall.Sockaddrは、ソケットアドレスを表すインターフェースで、OS固有のソケットアドレス構造体(例: syscall.SockaddrInet4syscall.SockaddrInet6)を抽象化します。
  • iosrv.ExecIO: Goの内部的なネットワークI/O処理において、iosrv(I/Oサービス)は、OS固有の非同期I/O操作を管理するためのコンポーネントです。ExecIO関数は、指定されたI/O操作(o)を実行し、必要に応じてデッドライン(fd.rdeadlineなど)を考慮に入れます。
  • netFD構造体: netFDは、Goのnetパッケージ内部で使用されるファイルディスクリプタ(またはソケットハンドル)を抽象化した構造体です。これには、読み込みデッドライン(rdeadline)や書き込みデッドライン(wdeadline)などの情報が含まれます。

技術的詳細

このコミットの技術的な核心は、Windows環境におけるnet.ListenerAcceptメソッドが、設定された読み込みデッドラインを適切に尊重するように修正することです。

以前の実装では、src/pkg/net/fd_windows.go内のnetFD.acceptメソッドにおいて、iosrv.ExecIOがデッドラインなしで呼び出されていました(_, err = iosrv.ExecIO(&o, 0))。これは、Accept操作がデッドラインに達してもタイムアウトせず、無限にブロックし続ける可能性を意味していました。

このコミットでは、iosrv.ExecIOの呼び出しを_, err = iosrv.ExecIO(&o, fd.rdeadline)に変更しています。ここで、fd.rdeadlinenetFD構造体に格納されている読み込みデッドラインの値です。この変更により、Accept操作がnetFDに設定された読み込みデッドラインを考慮するようになり、デッドラインを超過した場合には適切なエラー(通常はタイムアウトエラー)を返すようになります。

また、src/pkg/net/timeout_test.goTestTimeoutAcceptという新しいテストケースが追加されています。このテストは、net.Listenerに対して短いデッドラインを設定し、Acceptがそのデッドライン内でタイムアウトすることを確認します。これにより、修正が正しく機能していることが検証されます。特に、time.Afterselectステートメントを使用して、Acceptが指定された時間内にエラーを返すことを非同期的にチェックしています。

この修正は、WindowsのWinsock APIとGoの内部I/Oサービス層の間の連携を改善し、クロスプラットフォームでのnetパッケージのデッドラインセマンティクスの一貫性を高めるものです。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

  1. src/pkg/net/fd_windows.go:

    --- a/src/pkg/net/fd_windows.go
    +++ b/src/pkg/net/fd_windows.go
    @@ -544,7 +544,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {
     	var o acceptOp
     	o.Init(fd, 'r')
     	o.newsock = s
    -	_, err = iosrv.ExecIO(&o, 0)
    +	_, err = iosrv.ExecIO(&o, fd.rdeadline)
     	if err != nil {
     		closesocket(s)
     		return nil, err
    

    この変更は、netFDacceptメソッド内でiosrv.ExecIOを呼び出す際に、第2引数に0(デッドラインなし)を渡していたのを、fd.rdeadlinenetFDに設定された読み込みデッドライン)を渡すように修正しています。

  2. src/pkg/net/timeout_test.go:

    --- a/src/pkg/net/timeout_test.go
    +++ b/src/pkg/net/timeout_test.go
    @@ -119,3 +119,30 @@ func TestDeadlineReset(t *testing.T) {
     		t.Errorf("unexpected return from Accept; err=%v", err)
     	}
     }
    +
    +func TestTimeoutAccept(t *testing.T) {
    +	switch runtime.GOOS {
    +	case "plan9":
    +		t.Logf("skipping test on %q", runtime.GOOS)
    +		return
    +	}
    +	ln, err := Listen("tcp", "127.0.0.1:0")
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	defer ln.Close()
    +	tl := ln.(*TCPListener)
    +	tl.SetDeadline(time.Now().Add(100 * time.Millisecond))
    +	errc := make(chan error, 1)
    +	go func() {
    +		_, err := ln.Accept()
    +		errc <- err
    +	}()
    +	select {
    +	case <-time.After(1 * time.Second):
    +		// Accept shouldn't block indefinitely
    +		t.Errorf("Accept didn't return in an expected time")
    +	case <-errc:
    +		// Pass.
    +	}
    +}
    

    この変更は、TestTimeoutAcceptという新しいテスト関数を追加しています。このテストは、TCPリスナーを作成し、短いデッドラインを設定した後、Acceptがタイムアウトすることを確認します。

コアとなるコードの解説

src/pkg/net/fd_windows.goの変更

netFD.acceptメソッドは、Windows上で新しい接続を受け入れる際の低レベルな処理を担当します。このメソッド内で、iosrv.ExecIOは非同期I/O操作を実行するためのGoランタイムの内部関数です。

変更前: _, err = iosrv.ExecIO(&o, 0) 変更後: _, err = iosrv.ExecIO(&o, fd.rdeadline)

この変更は非常に重要です。iosrv.ExecIOの第2引数は、I/O操作のデッドラインを指定します。

  • 0を渡すことは、デッドラインが設定されていない、つまり操作が無限にブロックする可能性があることを意味します。
  • fd.rdeadlineを渡すことは、netFD(この場合はnet.Listenerの内部表現)に設定された読み込みデッドラインをAccept操作に適用することを意味します。

これにより、net.Listenerに対してSetDeadlineが呼び出された場合、そのデッドラインがAccept操作に伝播され、指定された時間内に接続が確立されない場合にAcceptがタイムアウトエラーを返すようになります。これは、Windows環境でのnet.ListenerAcceptが、他のOSと同様にデッドラインを尊重するようになるための修正です。

src/pkg/net/timeout_test.goの追加テスト

TestTimeoutAcceptは、この修正が正しく機能していることを検証するための統合テストです。

  1. OSチェック: runtime.GOOS"plan9"の場合はテストをスキップします。これは、この修正がWindows固有のものであるため、他のOSでの挙動は直接関係ないためです。
  2. リスナーの作成: Listen("tcp", "127.0.0.1:0")でTCPリスナーを作成します。"127.0.0.1:0"は、利用可能なポートを自動的に割り当ててローカルホストでリッスンすることを意味します。
  3. デッドラインの設定: tl.SetDeadline(time.Now().Add(100 * time.Millisecond))で、リスナーに100ミリ秒の短いデッドラインを設定します。これは、Acceptがこの時間内に接続を受け入れない場合にタイムアウトすることを期待するものです。
  4. Acceptの非同期呼び出し: go func() { _, err := ln.Accept(); errc <- err }()というgoroutine内でln.Accept()を呼び出します。これにより、Acceptがブロックしてもテストのメインスレッドがブロックされずに済みます。Acceptの結果(エラー)はerrcチャネルに送信されます。
  5. タイムアウトの検証: selectステートメントを使用して、Acceptがタイムアウトしたかどうかをチェックします。
    • case <-time.After(1 * time.Second): もし1秒経ってもerrcから何も受信されない場合、Acceptが期待以上に長くブロックしていると判断し、テストを失敗させます。
    • case <-errc: Acceptがエラーを返した場合(期待されるのはタイムアウトエラー)、テストは成功とみなされます(// Pass.)。

このテストは、Acceptがデッドラインを尊重し、適切な時間内にタイムアウトエラーを返すことを保証します。

関連リンク

参考にした情報源リンク

  • Go Issue #4296のコメントと議論
  • Go言語のnetパッケージのドキュメント
  • Go言語のsyscallパッケージのドキュメント
  • Go言語のtimeパッケージのドキュメント
  • Windows Winsock APIに関する一般的な情報