[インデックス 15426] ファイルの概要
このコミットは、Go言語の標準ライブラリtime
パッケージにおける、非常に大きなスリープ期間(Duration)の取り扱いに関するバグ修正を目的としています。具体的には、time.After
やtime.NewTimer
などの関数に渡される期間がint64
の最大値に近づくと発生する可能性のある整数オーバーフローの問題に対処しています。この修正により、タイマーが意図しない短い期間で発火するのを防ぎ、システムの安定性と予測可能性を向上させています。
コミット
commit 89cf67eb20bb863b87f4093e4eade2851dc9c308
Author: Andrew Gerrand <adg@golang.org>
Date: Tue Feb 26 09:23:58 2013 +1100
time: handle very large sleep durations
Fixes #4903.
R=golang-dev, daniel.morsing, dave, r
CC=golang-dev
https://golang.org/cl/7388056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/89cf67eb20bb863b87f4093e4eade2851dc9c308
元コミット内容
time: handle very large sleep durations
Fixes #4903.
R=golang-dev, daniel.morsing, dave, r
CC=golang-dev
https://golang.org/cl/7388056
変更の背景
この変更の背景には、Go言語のtime
パッケージが提供するタイマー機能において、非常に長い期間(例えば、int64
の最大値に近い期間)を指定した場合に発生する可能性のあるバグが存在していました。具体的には、タイマーが発火する「いつ (when)」の時刻を計算する際に、現在のナノ秒単位の時刻に指定された期間を加算すると、int64
型の範囲を超えてオーバーフローしてしまう問題です。
int64
のオーバーフローが発生すると、計算結果が負の値になったり、非常に小さな正の値になったりする可能性があります。これにより、ユーザーが数十年先やそれ以上の期間を指定したタイマーが、実際にはすぐに発火してしまうという予期せぬ動作を引き起こす可能性がありました。これは、特に長期稼働するシステムや、非常に長い間隔でイベントをスケジュールする必要があるアプリケーションにおいて、深刻な問題となり得ます。
コミットメッセージにある "Fixes #4903." は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。このコミットは、その報告された問題を解決するために導入されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と一般的なプログラミングの知識が必要です。
time
パッケージ: Go言語の標準ライブラリの一部で、時間に関する機能(現在時刻の取得、時間の計測、タイマー、スリープなど)を提供します。time.Duration
: 時間の長さを表す型で、ナノ秒単位のint64
として内部的に表現されます。例えば、time.Second
は1秒を表すDuration
です。time.Timer
: 指定された期間が経過した後に、単一のイベントを送信するオブジェクトです。NewTimer
関数で作成し、C
チャネルを通じてイベントを受け取ります。time.After
: 指定された期間が経過した後に、現在の時刻を送信するチャネルを返す便利な関数です。time.AfterFunc
: 指定された期間が経過した後に、関数を実行するタイマーを作成します。runtimeTimer
構造体:time
パッケージの内部でタイマーを管理するために使用される構造体です。この構造体には、タイマーが発火する時刻(when
フィールド)がナノ秒単位で格納されています。nano()
関数: 現在の時刻をナノ秒単位で返す内部関数です。- 整数オーバーフロー: 整数型で表現できる最大値を超えた場合に、その値が予期せぬ小さな値(または負の値)になる現象です。
int64
の場合、最大値は2^63 - 1
です。この値にさらに正の数を加算すると、最小値(負の値)に「ラップアラウンド」することがあります。 math.MaxInt64
: Go言語のmath
パッケージで定義されているint64
型の最大値です。1<<63 - 1
と同じ値です。
技術的詳細
このコミットの主要な技術的詳細は、time
パッケージ内部にwhen
という新しいヘルパー関数を導入し、タイマーの発火時刻を計算する際の整数オーバーフローを防ぐ点にあります。
when
ヘルパー関数の導入
以前のコードでは、タイマーの発火時刻はnano() + int64(d)
のように直接計算されていました。ここでnano()
は現在のナノ秒時刻、d
はタイマーの期間です。d
が非常に大きい場合、この加算がint64
の最大値を超えてオーバーフローし、結果として負の値になる可能性がありました。負の値は、タイマーが過去の時刻に設定されたと解釈され、即座に発火してしまう原因となります。
新しいwhen
関数は、このオーバーフロー問題を以下のように解決します。
// when is a helper function for setting the 'when' field of a runtimeTimer.
// It returns what the time will be, in nanoseconds, Duration d in the future.
// If d is negative, it is ignored. If the returned value would be less than
// zero because of an overflow, MaxInt64 is returned.
func when(d Duration) int64 {
if d <= 0 {
return nano()
}
t := nano() + int64(d)
if t < 0 { // オーバーフローチェック
t = 1<<63 - 1 // math.MaxInt64
}
return t
}
- 負の期間の処理:
d <= 0
の場合、タイマーは即座に発火すべきであるため、現在の時刻nano()
を返します。これは、負の期間が指定された場合の既存の動作を維持します。 - 通常の時刻計算:
t := nano() + int64(d)
で、通常通り発火時刻を計算します。 - オーバーフローチェック:
if t < 0
という条件でオーバーフローを検出します。nano()
は常に正の値であり、d
も正の期間であるため、nano() + int64(d)
の結果が負になるのは、int64
の最大値を超えてオーバーフローした場合のみです。 - オーバーフロー時の処理: オーバーフローが検出された場合、
t
をint64
の最大値である1<<63 - 1
(math.MaxInt64
)に設定します。これにより、タイマーは可能な限り未来の時刻に設定され、即座に発火するのを防ぎます。これは、事実上「非常に長い期間」を意味し、システムが許容する最長のスリープ期間として扱われます。
既存関数への適用
このwhen
ヘルパー関数は、time
パッケージ内でタイマーの発火時刻を設定する以下の主要な関数に適用されます。
NewTimer(d Duration) *Timer
: 新しいタイマーを作成する際に、runtimeTimer
のwhen
フィールドを設定するためにwhen(d)
が使用されます。(*Timer) Reset(d Duration) bool
: 既存のタイマーをリセットする際に、新しい発火時刻を設定するためにwhen(d)
が使用されます。AfterFunc(d Duration, f func()) *Timer
: 指定された期間後に実行される関数を設定するタイマーを作成する際に、runtimeTimer
のwhen
フィールドを設定するためにwhen(d)
が使用されます。
これらの変更により、Goのタイマー機能は、非常に大きな期間が指定された場合でも、オーバーフローによって誤動作することなく、意図した通りに動作するようになります。
テストケースの追加
src/pkg/time/sleep_test.go
には、この修正が正しく機能することを確認するための新しいテストケースTestOverflowSleep
が追加されています。
// Test that sleeping for an interval so large it overflows does not
// result in a short sleep duration.
func TestOverflowSleep(t *testing.T) {
const timeout = 25 * Millisecond
const big = Duration(int64(1<<63 - 1)) // int64の最大値
select {
case <-After(big):
t.Fatalf("big timeout fired") // オーバーフローしてすぐに発火したら失敗
case <-After(timeout):
// OK (25ミリ秒後に発火すればOK)
}
const neg = Duration(-1 << 63) // int64の最小値(負の最大値)
select {
case <-After(neg):
// OK (負の期間はすぐに発火すべき)
case <-After(timeout):
t.Fatalf("negative timeout didn't fire") // 負の期間なのに発火しなかったら失敗
}
}
このテストは、以下の2つのシナリオを検証します。
- 非常に大きな期間(オーバーフローする可能性のある値):
int64
の最大値に設定されたDuration
をAfter
関数に渡します。この場合、タイマーは非常に長い期間後に発火するように設定されるべきであり、すぐに発火してはなりません。テストは、短いtimeout
(25ミリ秒)が先に発火することを確認することで、オーバーフローによる早期発火がないことを検証します。 - 負の期間:
int64
の最小値(負の最大値)に設定されたDuration
をAfter
関数に渡します。負の期間は、タイマーが即座に発火すべきであることを意味します。テストは、After(neg)
がすぐに発火することを確認することで、この動作が期待通りであることを検証します。
これらのテストケースは、when
関数が正しくオーバーフローを処理し、負の期間も適切に扱うことを保証します。
コアとなるコードの変更箇所
src/pkg/time/sleep.go
とsrc/pkg/time/sleep_test.go
が変更されています。
diff --git a/src/pkg/time/sleep.go b/src/pkg/time/sleep.go
index 657b669030..591fa27b09 100644
--- a/src/pkg/time/sleep.go
+++ b/src/pkg/time/sleep.go
@@ -22,6 +22,21 @@ type runtimeTimer struct {
arg interface{}
}\n
+// when is a helper function for setting the 'when' field of a runtimeTimer.
+// It returns what the time will be, in nanoseconds, Duration d in the future.
+// If d is negative, it is ignored. If the returned value would be less than
+// zero because of an overflow, MaxInt64 is returned.
+func when(d Duration) int64 {
+\tif d <= 0 {
+\t\treturn nano()
+\t}
+\tt := nano() + int64(d)
+\tif t < 0 {
+\t\tt = 1<<63 - 1 // math.MaxInt64
+\t}
+\treturn t
+}\n+\n func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
\n@@ -49,7 +64,7 @@ func NewTimer(d Duration) *Timer {
\tt := &Timer{\n \t\tC: c,\n \t\tr: runtimeTimer{\n-\t\t\twhen: nano() + int64(d),\n+\t\t\twhen: when(d),\n \t\t\tf: sendTime,\n \t\t\targ: c,\n \t\t},\n@@ -62,9 +77,9 @@ func (t *Timer) Reset(d Duration) bool {
// It returns true if the timer had been active, false if the timer had
// expired or been stopped.\n func (t *Timer) Reset(d Duration) bool {
-\twhen := nano() + int64(d)\n+\tw := when(d)\n \tactive := stopTimer(&t.r)\n-\tt.r.when = when
+\tt.r.when = w
\tstartTimer(&t.r)\n \treturn active
}\n@@ -94,7 +109,7 @@ func After(d Duration) <-chan Time {
func AfterFunc(d Duration, f func()) *Timer {\n \tt := &Timer{\n \t\tr: runtimeTimer{\n-\t\t\twhen: nano() + int64(d),\n+\t\t\twhen: when(d),\n \t\t\tf: goFunc,\n \t\t\targ: f,\n \t\t},\ndiff --git a/src/pkg/time/sleep_test.go b/src/pkg/time/sleep_test.go
index bcdaffc2ac..9908e220f0 100644
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -293,3 +293,23 @@ func TestReset(t *testing.T) {\n \t}\n \tt.Error(err)\n }\n+\n+// Test that sleeping for an interval so large it overflows does not
+// result in a short sleep duration.
+func TestOverflowSleep(t *testing.T) {\n+\tconst timeout = 25 * Millisecond
+\tconst big = Duration(int64(1<<63 - 1))\n+\tselect {\n+\tcase <-After(big):\n+\t\tt.Fatalf(\"big timeout fired\")\n+\tcase <-After(timeout):\n+\t\t// OK\n+\t}\n+\tconst neg = Duration(-1 << 63)\n+\tselect {\n+\tcase <-After(neg):\n+\t\t// OK\n+\tcase <-After(timeout):\n+\t\tt.Fatalf(\"negative timeout didn't fire\")\n+\t}\n+}\n
コアとなるコードの解説
このコミットの核心は、src/pkg/time/sleep.go
に追加されたwhen
関数と、それが既存のタイマー関連関数にどのように統合されたかです。
when
関数の詳細
func when(d Duration) int64 {
if d <= 0 {
return nano()
}
t := nano() + int64(d)
if t < 0 {
t = 1<<63 - 1 // math.MaxInt64
}
return t
}
この関数は、タイマーが発火すべきナノ秒単位の絶対時刻を計算します。
d <= 0
の場合、期間がゼロまたは負であれば、タイマーは即座に発火すべきなので、現在のナノ秒時刻nano()
を返します。t := nano() + int64(d)
で、現在の時刻に指定された期間を加算して、発火時刻の候補t
を計算します。if t < 0
のチェックが重要です。nano()
は常に正の値であり、d
も正の期間として扱われるため、この加算結果が負になるのは、int64
の最大値を超えてオーバーフローした場合のみです。- オーバーフローが発生した場合(
t < 0
)、t
はint64
の最大値である1<<63 - 1
(math.MaxInt64
)に設定されます。これにより、タイマーは可能な限り未来の時刻に設定され、オーバーフローによる即時発火を防ぎます。
既存関数への適用
NewTimer
, Reset
, AfterFunc
の各関数では、以前はnano() + int64(d)
と直接記述されていたタイマーの発火時刻計算が、すべて新しく導入されたwhen(d)
関数に置き換えられました。
NewTimer
:// 変更前: // when: nano() + int64(d), // 変更後: when: when(d),
Reset
:// 変更前: // when := nano() + int64(d) // t.r.when = when // 変更後: w := when(d) t.r.when = w
AfterFunc
:// 変更前: // when: nano() + int64(d), // 変更後: when: when(d),
この変更により、タイマーの期間計算ロジックが一箇所に集約され、コードの保守性が向上するとともに、オーバーフロー対策が確実に適用されるようになりました。
テストコードの解説
TestOverflowSleep
は、この修正の有効性を検証するための重要なテストです。
const big = Duration(int64(1<<63 - 1))
は、int64
の最大値に相当する非常に大きな期間を定義しています。この期間をAfter
関数に渡した場合、オーバーフローがなければタイマーは非常に長い時間発火しないはずです。テストでは、25ミリ秒という短いtimeout
が先に発火することを確認することで、big
期間のタイマーがオーバーフローによって早期に発火しないことを検証しています。もしbig
タイマーが先に発火したら、それはバグ(オーバーフローによる早期発火)を意味します。const neg = Duration(-1 << 63)
は、int64
の最小値(負の最大値)に相当する負の期間を定義しています。when
関数は負の期間を現在の時刻として扱うため、After(neg)
は即座に発火するはずです。テストでは、After(neg)
がすぐに発火することを確認し、負の期間の処理が正しいことを検証しています。
これらのテストは、when
関数が意図した通りに、非常に大きな正の期間と負の期間の両方を適切に処理することを確認します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/89cf67eb20bb863b87f4093e4eade2851dc9c308
- Go CL (Code Review): https://golang.org/cl/7388056
参考にした情報源リンク
- Go言語
time
パッケージ公式ドキュメント: https://pkg.go.dev/time - Go言語
math
パッケージ公式ドキュメント: https://pkg.go.dev/math - 整数オーバーフローに関する一般的な情報 (例: Wikipediaなど)