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

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

このコミットは、Go言語の標準ライブラリtimeパッケージにおける、非常に大きなスリープ期間(Duration)の取り扱いに関するバグ修正を目的としています。具体的には、time.Aftertime.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
}
  1. 負の期間の処理: d <= 0の場合、タイマーは即座に発火すべきであるため、現在の時刻nano()を返します。これは、負の期間が指定された場合の既存の動作を維持します。
  2. 通常の時刻計算: t := nano() + int64(d)で、通常通り発火時刻を計算します。
  3. オーバーフローチェック: if t < 0という条件でオーバーフローを検出します。nano()は常に正の値であり、dも正の期間であるため、nano() + int64(d)の結果が負になるのは、int64の最大値を超えてオーバーフローした場合のみです。
  4. オーバーフロー時の処理: オーバーフローが検出された場合、tint64の最大値である1<<63 - 1math.MaxInt64)に設定します。これにより、タイマーは可能な限り未来の時刻に設定され、即座に発火するのを防ぎます。これは、事実上「非常に長い期間」を意味し、システムが許容する最長のスリープ期間として扱われます。

既存関数への適用

このwhenヘルパー関数は、timeパッケージ内でタイマーの発火時刻を設定する以下の主要な関数に適用されます。

  • NewTimer(d Duration) *Timer: 新しいタイマーを作成する際に、runtimeTimerwhenフィールドを設定するためにwhen(d)が使用されます。
  • (*Timer) Reset(d Duration) bool: 既存のタイマーをリセットする際に、新しい発火時刻を設定するためにwhen(d)が使用されます。
  • AfterFunc(d Duration, f func()) *Timer: 指定された期間後に実行される関数を設定するタイマーを作成する際に、runtimeTimerwhenフィールドを設定するために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つのシナリオを検証します。

  1. 非常に大きな期間(オーバーフローする可能性のある値): int64の最大値に設定されたDurationAfter関数に渡します。この場合、タイマーは非常に長い期間後に発火するように設定されるべきであり、すぐに発火してはなりません。テストは、短いtimeout(25ミリ秒)が先に発火することを確認することで、オーバーフローによる早期発火がないことを検証します。
  2. 負の期間: int64の最小値(負の最大値)に設定されたDurationAfter関数に渡します。負の期間は、タイマーが即座に発火すべきであることを意味します。テストは、After(neg)がすぐに発火することを確認することで、この動作が期待通りであることを検証します。

これらのテストケースは、when関数が正しくオーバーフローを処理し、負の期間も適切に扱うことを保証します。

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

src/pkg/time/sleep.gosrc/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)、tint64の最大値である1<<63 - 1math.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関数が意図した通りに、非常に大きな正の期間と負の期間の両方を適切に処理することを確認します。

関連リンク

参考にした情報源リンク

  • Go言語 time パッケージ公式ドキュメント: https://pkg.go.dev/time
  • Go言語 math パッケージ公式ドキュメント: https://pkg.go.dev/math
  • 整数オーバーフローに関する一般的な情報 (例: Wikipediaなど)