[インデックス 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ドメインソケットなど、様々なネットワークタイプに対応しています。
deadline
とtimeout
の概念
deadline
(デッドライン): 操作が完了しなければならない絶対的な時刻(time.Time
型)を指します。例えば、time.Now().Add(5 * time.Second)
は現在から5秒後をデッドラインとします。timeout
(タイムアウト): 操作が完了するまでに許容される相対的な時間量(time.Duration
型)を指します。例えば、5 * time.Second
は5秒間をタイムアウトとします。
deadline
が設定されている場合、timeout
はdeadline
から現在の時刻を引くことで計算されます。
resolveAndDialChannel
の役割
resolveAndDialChannel
関数は、Goのnet
パッケージ内部で使用される関数で、特にPlan 9や一部の古いWindowsバージョンなど、オペレーティングシステムが接続のデッドライン処理をGoのポーリングサーバーに完全にプッシュダウンしていない環境で利用されます。この関数は、アドレス解決(resolveAddr
)と実際の接続確立(dial
)をGoのランタイム側で管理し、タイムアウト処理もGoのタイマー機能を使って行います。
競合状態(Race Condition)
競合状態とは、複数の並行に実行されるプロセスやゴルーチンが共有リソース(この場合はネットワーク接続の状態やタイマー)にアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、タイムアウトの計算とアドレス解決の実行順序やタイミングによって、接続が成功したり失敗したりする可能性がありました。
time.NewTimer
とdefer 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
関数の変更点
-
タイムアウト計算の改善: 以前のコードでは、
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()) + }
これにより、デッドラインが設定されていない場合は
timeout
がtime.Duration
のゼロ値(0秒)のままとなり、後続の処理で適切に扱われます。 -
即時ダイヤル処理の追加:
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
が実行される前にタイマーが切れてしまうという問題が解消されます。 -
テスト用変数の導入:
var testingIssue5349 bool
というグローバル変数が導入されました。これは、テスト中に特定の競合状態を再現するために使用されます。 -
テスト時の遅延挿入: アドレス解決を行うゴルーチン内で、
testingIssue5349
がtrue
の場合に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
の場合にresolveAddr
とdial
を即座に実行することで、デッドラインが既に過ぎている状況で不必要なタイマーの起動とそれに伴う競合を排除します。これにより、アドレス解決が完了する前にタイマーが切れるという問題が根本的に解決されます。 - 正確なタイムアウト計算:
deadline.IsZero()
のチェックにより、デッドラインが設定されていない場合にtimeout
が不適切に0に設定されることを防ぎ、より正確なタイムアウト処理を可能にします。 - テストによる再現性:
testingIssue5349
とそれに伴う遅延の挿入により、開発者がこの種の競合状態を容易に再現し、修正が有効であることを確認できるようになりました。
コアとなるコードの変更箇所
src/pkg/net/dial_gen.go
のresolveAndDialChannel
関数における変更点:
--- 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
-
var testingIssue5349 bool // used during tests
resolveAndDialChannel
関数の冒頭に、テスト目的で使用されるブール型変数testingIssue5349
が追加されました。これは、特定のテストシナリオで競合状態を再現するために利用されます。
-
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
を起動せずに、すぐにresolveAddr
とdial
を実行します。 resolveAddr("dial", net, addr, noDeadline)
: アドレス解決を行います。noDeadline
は、このアドレス解決自体にはデッドラインを適用しないことを意味します。- エラーが発生した場合は即座にエラーを返します。
return dial(net, addr, localAddr, ra, noDeadline)
: 解決されたアドレスra
を使用して実際の接続を試みます。ここでもnoDeadline
が使用されます。- この即時処理により、デッドラインが既に過ぎているにもかかわらず、時間のかかるアドレス解決がタイマーの起動と並行して行われ、タイマーが先に切れてしまうという競合状態が回避されます。
- もし計算された
- 以前は
-
go func() { ... }
内のif testingIssue5349 { time.Sleep(time.Millisecond) }
- アドレス解決(
resolveAddr
)を実行するゴルーチンの中に、テスト用の遅延が挿入されました。 testingIssue5349
がtrue
の場合(テスト実行時)、resolveAddr
の呼び出しの直前に1ミリ秒のスリープが入ります。- このわずかな遅延により、メインのゴルーチンで
time.NewTimer
が起動し、そのタイマーが発火する前にアドレス解決が完了しないという状況を意図的に作り出し、競合状態を再現しやすくします。これにより、修正が正しく機能していることを検証できます。
- アドレス解決(
src/pkg/net/dial_gen_test.go
-
// +build windows plan9
- この行はビルドタグです。このファイルは、GoのビルドシステムによってWindowsまたはPlan 9環境でのみコンパイルされることを示します。これは、この修正がこれらの特定のOS環境に特化しているためです。
-
package net
net
パッケージの一部としてテストが定義されています。
-
func init() { testingIssue5349 = true }
init
関数は、パッケージが初期化される際に自動的に実行されます。- この
init
関数内でtestingIssue5349
がtrue
に設定されます。これにより、このテストが実行される環境では、dial_gen.go
内のresolveAndDialChannel
関数で意図的な遅延が有効になり、競合状態のテストが可能になります。
これらの変更により、net.Dial
がPlan 9およびWindows環境でデッドラインが設定されている場合に発生していた競合状態が解消され、より堅牢なネットワーク接続処理が実現されました。
関連リンク
- Go Issue 5349: https://github.com/golang/go/issues/5349
- Go CL 9159043: https://golang.org/cl/9159043
参考にした情報源リンク
- https://github.com/golang/go/commit/e1922febbec63414db8f756775d4369797775264
- https://github.com/golang/go/issues/5349
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEw8PqFRglgKj22NehONDJ3kdHP1NBW-Sg139G4-GvcpNf1-1CWBskverGszKHMIFNDRwk7MGOCvWdvQkT_XBjFMeDwqWhD87I6oFhxoWio_B52ZLNBa95_pcxrv8wP3ydsuN8= (Web検索結果より)
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEgSLnaxXziGZQtUnJAniZaD5mHJ2DtyZlyDDfj3nNEcS-C6hIQWZYuEILF6xvsW0FVufnh7UxJ-Xt01vPgMzxSf-ZRIAfpOnFj6ncBkiw33KORIEsegTVbr-SI1oMIFDqGDfBo (Web検索結果より)
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGUriT9PvEnTLKrioRvq2RnQUZIRxPefNzy1r0J133I5Z_LHz5J0Sj71vdbV8DwPAKWRHsD7MiovYBqsAUbMCl3OgDg0kvRMv9qDFEaFC9mwg6_5TJO435Mkl9LLlNQ5dYp-G0vmEpBEda_p_wK3ixc_b2a00iykXSmIU0XDba9z445fPpUx24wYfhm8Lb7gSt_rShaSLqvOQ9Aal7Qm3tVTzo= (Web検索結果より)
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHCmhSkll4olingYLq1hUTV1GZKde8Aj4bvwK28Pp4ErL7Y6w7anH_ani-QwNQ3WAW4ufYOI1o7J7XYYFPpGBh3CQWEVJfpuDPrax1OZL0y4Y417e56_9_qq6ySEOctvAyonKj8dDv3vDAvMUZIuk7N29EwFdsbVltCYZyJcm7-jq0osj3RKbXwMZKDyxQh7I9SPRQXm7v0kFKEn7v9k_lEbxaXz9RuCZalmb5Ly6_vRQ6-j7q--zPT-CJhQag= (Web検索結果より)