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

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

このコミットは、Go言語のnetパッケージにおいて、Plan 9およびWindows環境で発生していたDial処理における競合状態(race condition)を修正するものです。具体的には、resolveAndDialChannel関数におけるタイムアウトの計算とアドレス解決のロジックが改善され、特定の条件下で発生する不適切なタイムアウトや競合が解消されました。

コミット

  • コミットハッシュ: e1922febbec63414db8f756775d4369797775264
  • Author: Alex Brainman alex.brainman@gmail.com
  • Date: Wed May 8 16:19:02 2013 +1000
  • コミットメッセージ:
    net: fix dial race on plan9 and windows
    
    Fixes #5349.
    
    R=golang-dev, lucio.dere, dsymonds, bradfitz, iant, adg, dave, r
    CC=golang-dev
    https://golang.org/cl/9159043
    

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

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

元コミット内容

net: fix dial race on plan9 and windows

Fixes #5349.

R=golang-dev, lucio.dere, dsymonds, bradfitz, iant, adg, dave, r
CC=golang-dev
https://golang.org/cl/9159043

変更の背景

この変更は、GoのnetパッケージにおけるDial操作、特にPlan 9とWindowsといった特定のオペレーティングシステム環境で発生していた競合状態を解決するために行われました。コミットメッセージに「Fixes #5349」とあるように、GoのIssue 5349に関連しています。

Issue 5349自体は、UDP接続においてnet.DialがWindows上で即座にタイムアウトしてしまう問題として報告されていました。この問題は、Dial操作が内部でアドレス解決(resolveAddr)と実際の接続(dial)の2つのフェーズに分かれていることに起因します。特に、deadline(接続の最終期限)が設定されている場合、resolveAndDialChannel関数内でタイムアウトが不適切に計算されることで、アドレス解決が完了する前にタイマーが切れてしまい、接続が失敗するという競合状態が発生していました。

具体的には、deadlineが過去の時刻であるか、非常に近い未来の時刻である場合、deadline.Sub(time.Now())の結果が0以下になることがあります。この場合、以前のコードではtimeoutが強制的に0に設定されていました。しかし、timeoutが0であるにもかかわらず、その後に時間のかかるresolveAddrが実行されるため、resolveAddrが完了する前にタイマーが切れてしまい、接続試行がキャンセルされてしまうという問題がありました。これは、アドレス解決とタイムアウト処理の間に発生する時間的な競合であり、特にネットワークの状態やシステム負荷によって顕在化しやすい問題でした。

この競合状態は、特にPlan 9や一部の古いWindowsバージョンで顕著でした。これらのシステムでは、接続のデッドライン処理がGoのポーリングサーバー(pollserver)に完全にプッシュダウンされておらず、Goのランタイム側でタイムアウトを管理する必要があったため、より影響を受けやすかったと考えられます。

前提知識の解説

Go言語のnetパッケージとDial関数

Go言語のnetパッケージは、ネットワークI/Oのプリミティブを提供します。net.Dial関数は、指定されたネットワークアドレスへの接続を確立するために使用される最も一般的な関数の一つです。これはTCP、UDP、Unixドメインソケットなど、様々なネットワークタイプに対応しています。

deadlinetimeoutの概念

  • deadline (デッドライン): 操作が完了しなければならない絶対的な時刻(time.Time型)を指します。例えば、time.Now().Add(5 * time.Second)は現在から5秒後をデッドラインとします。
  • timeout (タイムアウト): 操作が完了するまでに許容される相対的な時間量(time.Duration型)を指します。例えば、5 * time.Secondは5秒間をタイムアウトとします。

deadlineが設定されている場合、timeoutdeadlineから現在の時刻を引くことで計算されます。

resolveAndDialChannelの役割

resolveAndDialChannel関数は、Goのnetパッケージ内部で使用される関数で、特にPlan 9や一部の古いWindowsバージョンなど、オペレーティングシステムが接続のデッドライン処理をGoのポーリングサーバーに完全にプッシュダウンしていない環境で利用されます。この関数は、アドレス解決(resolveAddr)と実際の接続確立(dial)をGoのランタイム側で管理し、タイムアウト処理もGoのタイマー機能を使って行います。

競合状態(Race Condition)

競合状態とは、複数の並行に実行されるプロセスやゴルーチンが共有リソース(この場合はネットワーク接続の状態やタイマー)にアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、タイムアウトの計算とアドレス解決の実行順序やタイミングによって、接続が成功したり失敗したりする可能性がありました。

time.NewTimerdefer t.Stop()

Goのtimeパッケージのtime.NewTimer(d)は、指定された期間dが経過した後にチャネルに値を送信するタイマーを作成します。defer t.Stop()は、関数が終了する際にタイマーを停止し、タイマーがまだ発火していない場合にリソースを解放するために使用される一般的なパターンです。これにより、不要なタイマーイベントの発生を防ぎ、リソースリークを回避します。

技術的詳細

このコミットは、src/pkg/net/dial_gen.go内のresolveAndDialChannel関数と、新しく追加されたテストファイルsrc/pkg/net/dial_gen_test.goに変更を加えています。

resolveAndDialChannel関数の変更点

  1. タイムアウト計算の改善: 以前のコードでは、deadline.Sub(time.Now())の結果が負の値になった場合、timeoutを強制的に0に設定していました。

    -	timeout := deadline.Sub(time.Now())
    -	if timeout < 0 {
    -		timeout = 0
    -	}
    

    新しいコードでは、まずdeadline.IsZero()でデッドラインが設定されているかを確認します。

    +	var timeout time.Duration
    +	if !deadline.IsZero() {
    +		timeout = deadline.Sub(time.Now())
    +	}
    

    これにより、デッドラインが設定されていない場合はtimeouttime.Durationのゼロ値(0秒)のままとなり、後続の処理で適切に扱われます。

  2. 即時ダイヤル処理の追加: timeoutが0以下の場合(つまり、デッドラインが過去であるか、デッドラインが設定されていない場合)、アドレス解決とダイヤルを即座に実行するロジックが追加されました。

    +	if timeout <= 0 {
    +		ra, err := resolveAddr("dial", net, addr, noDeadline)
    +		if err != nil {
    +			return nil, err
    +		}
    +		return dial(net, addr, localAddr, ra, noDeadline)
    +	}
    

    この変更の意図は、デッドラインが既に過ぎている、またはデッドラインが設定されていない場合に、時間のかかるtime.NewTimerの生成やゴルーチンの起動をスキップし、すぐにアドレス解決と接続試行を行うことで、不必要なタイムアウトや競合状態を回避することです。これにより、resolveAddrが実行される前にタイマーが切れてしまうという問題が解消されます。

  3. テスト用変数の導入: var testingIssue5349 boolというグローバル変数が導入されました。これは、テスト中に特定の競合状態を再現するために使用されます。

  4. テスト時の遅延挿入: アドレス解決を行うゴルーチン内で、testingIssue5349trueの場合にtime.Sleep(time.Millisecond)が挿入されるようになりました。

    +	if testingIssue5349 {
    +		time.Sleep(time.Millisecond)
    +	}
    

    この遅延は、アドレス解決(resolveAddr)が完了する前にメインのゴルーチンでタイマーが発火する可能性を高め、競合状態を意図的に再現するために利用されます。

src/pkg/net/dial_gen_test.goの追加

新しいテストファイルdial_gen_test.goが追加されました。このファイルは、Plan 9とWindows環境でのみビルドされるように// +build windows plan9というビルドタグが付けられています。

このテストファイルにはinit関数が含まれており、その中でtestingIssue5349 = trueが設定されます。

// +build windows plan9

package net

func init() {
	testingIssue5349 = true
}

これにより、テスト実行時にresolveAndDialChannel内の意図的な遅延が有効になり、Issue 5349で報告されたような競合状態がテスト環境で再現されやすくなります。このテストの追加により、修正が正しく機能していることを検証できるようになりました。

競合状態の修正メカニズム

このコミットによる競合状態の修正は、主に以下の点に基づいています。

  • 即時ダイヤル: timeout <= 0の場合にresolveAddrdialを即座に実行することで、デッドラインが既に過ぎている状況で不必要なタイマーの起動とそれに伴う競合を排除します。これにより、アドレス解決が完了する前にタイマーが切れるという問題が根本的に解決されます。
  • 正確なタイムアウト計算: deadline.IsZero()のチェックにより、デッドラインが設定されていない場合にtimeoutが不適切に0に設定されることを防ぎ、より正確なタイムアウト処理を可能にします。
  • テストによる再現性: testingIssue5349とそれに伴う遅延の挿入により、開発者がこの種の競合状態を容易に再現し、修正が有効であることを確認できるようになりました。

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

src/pkg/net/dial_gen.goresolveAndDialChannel関数における変更点:

--- a/src/pkg/net/dial_gen.go
+++ b/src/pkg/net/dial_gen.go
@@ -10,14 +10,23 @@ import (
 	"time"
 )
 
+var testingIssue5349 bool // used during tests
+
 // resolveAndDialChannel is the simple pure-Go implementation of
 // resolveAndDial, still used on operating systems where the deadline
 // hasn't been pushed down into the pollserver. (Plan 9 and some old
 // versions of Windows)
 func resolveAndDialChannel(net, addr string, localAddr Addr, deadline time.Time) (Conn, error) {
-	timeout := deadline.Sub(time.Now())
-	if timeout < 0 {
-		timeout = 0
+	var timeout time.Duration
+	if !deadline.IsZero() {
+		timeout = deadline.Sub(time.Now())
+	}
+	if timeout <= 0 {
+		ra, err := resolveAddr("dial", net, addr, noDeadline)
+		if err != nil {
+			return nil, err
+		}
+		return dial(net, addr, localAddr, ra, noDeadline)
 	}
 	t := time.NewTimer(timeout)
 	defer t.Stop()
@@ -28,6 +37,9 @@ func resolveAndDialChannel(net, addr string, localAddr Addr, deadline time.Time) (Conn, error) {
 	ch := make(chan pair, 1)
 	resolvedAddr := make(chan Addr, 1)
 	go func() {
+		if testingIssue5349 {
+			time.Sleep(time.Millisecond)
+		}
 		ra, err := resolveAddr("dial", net, addr, noDeadline)
 		if err != nil {
 			ch <- pair{nil, err}

src/pkg/net/dial_gen_test.goの新規追加:

--- /dev/null
+++ b/src/pkg/net/dial_gen_test.go
@@ -0,0 +1,11 @@
+// Copyright 2013 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows plan9
+
+package net
+
+func init() {
+	testingIssue5349 = true
+}

コアとなるコードの解説

src/pkg/net/dial_gen.go

  1. var testingIssue5349 bool // used during tests

    • resolveAndDialChannel関数の冒頭に、テスト目的で使用されるブール型変数testingIssue5349が追加されました。これは、特定のテストシナリオで競合状態を再現するために利用されます。
  2. var timeout time.Duration から if timeout <= 0 { ... } のブロック

    • 以前はtimeout := deadline.Sub(time.Now())で直接タイムアウトを計算し、負の値なら0に設定していました。
    • 変更後、まずtimeoutをゼロ値で宣言し、if !deadline.IsZero()deadlineが有効な値であるかを確認します。これにより、deadlineが設定されていない場合に不適切なタイムアウト計算が行われるのを防ぎます。
    • 最も重要な変更は、if timeout <= 0 { ... }ブロックの追加です。
      • もし計算されたtimeoutが0以下(つまり、デッドラインが既に過ぎているか、デッドラインが設定されていない)であれば、time.NewTimerを起動せずに、すぐにresolveAddrdialを実行します。
      • resolveAddr("dial", net, addr, noDeadline): アドレス解決を行います。noDeadlineは、このアドレス解決自体にはデッドラインを適用しないことを意味します。
      • エラーが発生した場合は即座にエラーを返します。
      • return dial(net, addr, localAddr, ra, noDeadline): 解決されたアドレスraを使用して実際の接続を試みます。ここでもnoDeadlineが使用されます。
      • この即時処理により、デッドラインが既に過ぎているにもかかわらず、時間のかかるアドレス解決がタイマーの起動と並行して行われ、タイマーが先に切れてしまうという競合状態が回避されます。
  3. go func() { ... } 内の if testingIssue5349 { time.Sleep(time.Millisecond) }

    • アドレス解決(resolveAddr)を実行するゴルーチンの中に、テスト用の遅延が挿入されました。
    • testingIssue5349trueの場合(テスト実行時)、resolveAddrの呼び出しの直前に1ミリ秒のスリープが入ります。
    • このわずかな遅延により、メインのゴルーチンでtime.NewTimerが起動し、そのタイマーが発火する前にアドレス解決が完了しないという状況を意図的に作り出し、競合状態を再現しやすくします。これにより、修正が正しく機能していることを検証できます。

src/pkg/net/dial_gen_test.go

  1. // +build windows plan9

    • この行はビルドタグです。このファイルは、GoのビルドシステムによってWindowsまたはPlan 9環境でのみコンパイルされることを示します。これは、この修正がこれらの特定のOS環境に特化しているためです。
  2. package net

    • netパッケージの一部としてテストが定義されています。
  3. func init() { testingIssue5349 = true }

    • init関数は、パッケージが初期化される際に自動的に実行されます。
    • このinit関数内でtestingIssue5349trueに設定されます。これにより、このテストが実行される環境では、dial_gen.go内のresolveAndDialChannel関数で意図的な遅延が有効になり、競合状態のテストが可能になります。

これらの変更により、net.DialがPlan 9およびWindows環境でデッドラインが設定されている場合に発生していた競合状態が解消され、より堅牢なネットワーク接続処理が実現されました。

関連リンク

参考にした情報源リンク