[インデックス 1403] ファイルの概要
このコミットは、Go言語の標準ライブラリ time
パッケージ内の time.Tick
関数のテスト (tick_test.go
) において、テストの安定性を向上させるための変更です。具体的には、テストで使用されるティック間隔を10ミリ秒から100ミリ秒に延長することで、システムが高負荷状態にある場合でもテストが安定してパスするように修正されています。
コミット
commit be76898cb31a4a9da97965cfa753685d560874e7
Author: Russ Cox <rsc@golang.org>
Date: Mon Jan 5 11:18:20 2009 -0800
change time.Tick test to use 100ms intervals.
now passes even under loaded conditions on r45.
R=r
DELTA=2 (0 added, 0 deleted, 2 changed)
OCL=22019
CL=22022
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/be76898cb31a4a9da97965cfa753685d560874e7
元コミット内容
change time.Tick test to use 100ms intervals.
now passes even under loaded conditions on r45.
変更の背景
このコミットの背景には、time.Tick
関数のテストが、システムが高負荷状態("loaded conditions")にある環境、特に当時のGoのリリースバージョンである r45
で不安定になるという問題がありました。
time.Tick
のような時間依存の機能は、システムのCPU使用率が高い、I/Oが集中している、あるいはスケジューリングの遅延が発生しているといった高負荷条件下では、期待通りの厳密なタイミングでイベントが発生しないことがあります。テストが非常に短い間隔(この場合は10ミリ秒)に依存していると、わずかな遅延でもテストが失敗する「flaky test」(不安定なテスト)となる可能性が高まります。
開発者は、テストが実際のコードの振る舞いを正確に反映しつつ、実行環境の変動に左右されずに安定してパスすることを望みます。このコミットは、テストの信頼性を高め、高負荷時でもテストが誤って失敗しないようにするために、ティック間隔を長くするというアプローチが取られました。
前提知識の解説
time.Tick
関数
Go言語の time
パッケージは、時間に関する機能を提供します。time.Tick
関数は、指定された期間ごとにイベントを発生させるチャネルを返します。
func Tick(d Duration) <-chan Time
Tick
関数は d
で指定された期間(time.Duration
型)が経過するたびに、現在の時刻をチャネルに送信します。これは、定期的な処理(例: ログの出力、メトリクスの収集、定期的なデータ同期など)を実装する際によく使用されます。
時間依存コードのテストの課題
time.Tick
のような時間依存のコードをテストすることは、いくつかの課題を伴います。
- 非決定性 (Non-Determinism): テストがシステムの実時間(リアルタイムクロック)に依存するため、テストの実行タイミング、システム負荷、あるいは夏時間のような外部要因によって結果が変動する可能性があります。これにより、同じコードでもテストがパスしたり失敗したりする「flaky test」が発生しやすくなります。
- テストの遅延 (Slow Tests): 実際の時間が経過するのを待つ必要があるため、テストの実行に時間がかかります。これは、開発サイクルを遅らせ、継続的インテグレーションの効率を低下させます。
- 状態の検証の難しさ: 特定の期間内にイベントが発生したか、あるいは発生しなかったかを厳密に検証することが難しい場合があります。
これらの課題に対処するため、一般的には「モッククロック」や「フェイククロック」を用いた依存性注入(Dependency Injection)が推奨されます。これにより、テスト中に時間をプログラムで制御し、テストを決定論的かつ高速に実行できます。しかし、このコミットが作成された初期のGoのテストフレームワークでは、このような高度なモック化のパターンがまだ確立されていなかったか、あるいはテストの単純性を優先した可能性があります。
「Loaded Conditions」の概念
「Loaded Conditions」(高負荷状態)とは、システムがCPU、メモリ、I/Oなどのリソースを大量に消費している状態を指します。このような状況下では、オペレーティングシステムはプロセスのスケジューリングに遅延を生じさせたり、リソースの競合が発生したりすることがあります。
time.Tick
のようなタイマー機能は、OSのスケジューラに依存して正確なタイミングを保証します。システムが高負荷状態にあると、スケジューラがGoのランタイムやテストプロセスにCPU時間を割り当てるのが遅れ、結果として time.Tick
が期待される厳密な間隔よりも遅れてイベントを発生させることがあります。
このコミットでは、テストがこのような高負荷状態でも安定してパスするように、許容される時間的な「ずれ」の範囲を広げるために、ティック間隔自体を長くするというアプローチが取られました。
技術的詳細
このコミットの技術的な変更は非常にシンプルですが、その背後には時間依存のテストにおける現実的な課題への対処があります。
src/lib/time/tick_test.go
内の TestTick
関数は、time.Tick
が指定された回数だけ正確にティックを生成したかどうかを検証しています。テストは、Count
回のティックが Delta * Count
ナノ秒の合計時間内に収まることを期待します。そして、この合計時間に対して slop
(許容誤差)を設定し、実際の経過時間が target - slop
から target + slop
の範囲内にあるかをチェックします。
元のコードでは、Delta
が 10*1e6
ナノ秒(10ミリ秒)に設定されていました。これは非常に短い間隔であり、システムが高負荷状態にある場合、OSのスケジューリングの遅延やその他の要因により、実際のティック間隔がわずかに長くなり、テストが設定された slop
の範囲を超えて失敗する可能性がありました。
このコミットでは、Delta
の値を 10*1e6
から 100*1e6
ナノ秒(100ミリ秒)に増やしています。
Delta
の増加: ティック間隔を10倍にすることで、各ティックの間に許容される絶対的な時間的ずれの割合が相対的に小さくなります。例えば、10ミリ秒のティックで1ミリ秒の遅延は10%のずれですが、100ミリ秒のティックで1ミリ秒の遅延は1%のずれに過ぎません。これにより、同じ絶対的な遅延が発生しても、テストが設定されたslop
の範囲内に収まる可能性が高まります。- テストの安定性向上: 結果として、システムが高負荷状態にある場合でも、
time.Tick
のテストがより安定してパスするようになります。これは、テストが実際のシステム環境の変動に対してより堅牢になることを意味します。 - トレードオフ: この変更はテストの安定性を向上させますが、テストの粒度を粗くするというトレードオフがあります。つまり、より長い間隔でのみ
time.Tick
の正確性を検証することになり、非常に短い間隔での厳密なタイミング保証に関する問題を見逃す可能性はあります。しかし、このテストの目的がtime.Tick
の基本的な機能と、ある程度の時間的正確性を検証することであるならば、このトレードオフは許容範囲内と判断されたのでしょう。
また、t.Fatalf
のフォーマット文字列が %d
から %g
に変更され、Delta
, ns
, target
の値が float64
にキャストされています。これは、大きな数値(ナノ秒単位)をより読みやすく表示するため、あるいは将来的に Delta
が浮動小数点数になる可能性を考慮した、表示上の改善と考えられます。
コアとなるコードの変更箇所
--- a/src/lib/time/tick_test.go
+++ b/src/lib/time/tick_test.go
@@ -11,7 +11,7 @@ import (
export func TestTick(t *testing.T) {
const (
- Delta uint64 = 10*1e6;
+ Delta uint64 = 100*1e6;
Count uint64 = 10;
);
c := Tick(Delta);
@@ -24,6 +24,6 @@ export func TestTick(t *testing.T) {
target := int64(Delta*Count);
slop := target*2/10;
if ns < target - slop || ns > target + slop {
-\t\tt.Fatalf("%d ticks of %d ns took %d ns, expected %d", Count, Delta, ns, target);\n+\t\tt.Fatalf("%d ticks of %g ns took %g ns, expected %g", Count, float64(Delta), float64(ns), float64(target));
}\n }\
コアとなるコードの解説
変更は src/lib/time/tick_test.go
ファイル内の TestTick
関数に集中しています。
-
Delta
定数の変更:- Delta uint64 = 10*1e6; + Delta uint64 = 100*1e6;
ここがこのコミットの主要な変更点です。
Delta
はtime.Tick
関数に渡されるティック間隔(ナノ秒単位)を定義しています。- 変更前:
10*1e6
は10,000,000
ナノ秒、つまり10
ミリ秒です。 - 変更後:
100*1e6
は100,000,000
ナノ秒、つまり100
ミリ秒です。 この変更により、テストは10ミリ秒間隔ではなく、100ミリ秒間隔でティックを待ち受けるようになります。これにより、システムが高負荷状態にある場合でも、個々のティックの遅延が相対的に小さくなり、テストが設定された許容誤差 (slop
) の範囲内でパスしやすくなります。
- 変更前:
-
t.Fatalf
フォーマット文字列の変更:-\t\tt.Fatalf("%d ticks of %d ns took %d ns, expected %d", Count, Delta, ns, target); +\t\tt.Fatalf("%d ticks of %g ns took %g ns, expected %g", Count, float64(Delta), float64(ns), float64(target));
この変更は、テストが失敗した際に表示されるエラーメッセージのフォーマットを調整しています。
- 変更前は、
%d
フォーマット指定子を使用してuint64
やint64
の整数値を表示していました。 - 変更後は、
%g
フォーマット指定子を使用し、Delta
,ns
,target
の値をfloat64
に明示的にキャストしています。%g
は、数値の大きさに応じて%e
(指数表記)または%f
(浮動小数点表記)を自動的に選択する汎用的な浮動小数点フォーマット指定子です。これにより、非常に大きなナノ秒の値をより読みやすい形式で出力できるようになります。これは機能的な変更ではなく、デバッグ時の可読性を向上させるための改善です。
- 変更前は、
これらの変更は、time.Tick
のテストが、より現実的な(高負荷時の)環境下で安定して動作することを目指しています。
関連リンク
- Go言語
time
パッケージ公式ドキュメント: https://pkg.go.dev/time - Go言語
testing
パッケージ公式ドキュメント: https://pkg.go.dev/testing
参考にした情報源リンク
- dmitryfrank.com: https://dmitryfrank.com/articles/mocking_time_in_go
- coder.com: https://coder.com/blog/deterministic-time-testing-in-go
- go.dev: https://go.dev/blog/go1.24-synctest
- github.com (benbjohnson/clock): https://github.com/benbjohnson/clock
- medium.com (Mocking time in Go): https://medium.com/@benbjohnson/mocking-time-in-go-36720179b586
- smarty.com: https://www.smarty.com/articles/go-time-mocking
- ekm.id.au: https://ekm.id.au/2019/03/mocking-time-in-go-tests/
- medium.com (Testing time in Go): https://medium.com/@kyle.j.gorman/testing-time-in-go-1000000000000000
- github.com (load testing Go): https://github.com/tsenart/vegeta
- callistaenterprise.se: https://www.callistaenterprise.se/blogg/2020/03/17/load-testing-go-applications-with-vegeta/