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

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

このコミットは、Goランタイムのスタック成長テストにおけるタイムアウト値を調整するものです。具体的には、src/pkg/runtime/stack_test.go ファイル内の TestStackGrowth 関数において、ファイナライザの実行を待つタイムアウトが4秒から20秒に延長されています。これは、低速なビルド環境においてテストが不安定になる(flaky failures)問題を解消することを目的としています。

コミット

commit 72185093f669eab9b07523bded3d186b4bb4321d
Author: Russ Cox <rsc@golang.org>
Date:   Sun Apr 13 20:19:10 2014 -0400

    runtime: increase timeout in TestStackGrowth
    
    It looks like maybe on slower builders 4 seconds is not enough.
    Trying to get rid of the flaky failures.
    
    TBR=iant
    CC=golang-codereviews
    https://golang.org/cl/86870044

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

https://github.com/golang/go/commit/72185093f669eab9b07523bded3d186b4bb4321d

元コミット内容

runtime: increase timeout in TestStackGrowth

It looks like maybe on slower builders 4 seconds is not enough.
Trying to get rid of the flaky failures.

TBR=iant
CC=golang-codereviews
https://golang.org/cl/86870044

変更の背景

この変更の背景には、Go言語のテストインフラストラクチャにおける「flaky failures(不安定な失敗)」の問題があります。flaky failureとは、コード自体にバグがないにもかかわらず、特定の環境やタイミングの要因によってテストが時々失敗する現象を指します。

このコミットでは、TestStackGrowthというテストが、低速なビルド環境(slower builders)でタイムアウトにより失敗することが確認されました。TestStackGrowthは、Goランタイムがどのようにスタックを動的に拡張するかを検証するテストであり、その過程でガーベージコレクション(GC)とファイナライザの実行が関与します。

ファイナライザは、オブジェクトがGCによって回収される直前に実行される関数です。GCの実行タイミングは、システムの負荷やメモリ使用状況によって変動するため、低速な環境ではファイナライザの実行が遅延する可能性があります。元のタイムアウト値である4秒では、このような遅延に対応しきれず、テストが意図せず失敗していたと考えられます。

開発チームは、この不安定なテストの失敗を解消するために、タイムアウト値を延長するというアプローチを選択しました。これにより、テストがより堅牢になり、実際のバグによる失敗と環境要因による失敗を区別しやすくなります。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、スケジューラ(ゴルーチンの管理)、ガーベージコレクタ(メモリ管理)、スタック管理、プリミティブな同期メカニズムなどが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部でGoランタイムが軽量なゴルーチンをスケジューリングし、メモリを効率的に管理することで、高い並行性とパフォーマンスを実現しています。

スタック (Stack)

プログラムの実行において、スタックは関数呼び出しの情報を格納するためのメモリ領域です。関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタックに積まれます(プッシュ)。関数が終了すると、これらの情報はスタックから取り除かれます(ポップ)。

Goのスタック成長 (Go Stack Growth)

Go言語のゴルーチンは、非常に軽量であり、初期スタックサイズは比較的小さく設定されています(例えば、数KB)。これは、数百万ものゴルーチンを同時に実行できるようにするためです。しかし、関数呼び出しが深くネストされたり、大きなローカル変数が使用されたりすると、スタックが不足する可能性があります。

Goランタイムは、このような状況に対応するために「スタック成長(Stack Growth)」メカニズムを持っています。これは、スタックが不足しそうになったときに、ランタイムが自動的にスタック領域を拡張する機能です。Goのスタックは、連続したメモリ領域ではなく、必要に応じて新しいセグメントを割り当ててリンクすることで、動的に拡張されます。このプロセスは、プログラムの実行中に透過的に行われます。

ガーベージコレクション (Garbage Collection, GC)

ガーベージコレクションは、プログラムが動的に確保したメモリのうち、もはや使用されていない(参照されていない)領域を自動的に解放するプロセスです。GoのGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作することで、アプリケーションの一時停止(Stop-the-World)時間を最小限に抑えるように設計されています。

ファイナライザ (Finalizer)

Goのファイナライザは、runtime.SetFinalizer 関数を使って設定される特殊な関数です。あるオブジェクトにファイナライザが設定されている場合、そのオブジェクトがガーベージコレクタによって到達不可能(つまり、どの変数からも参照されなくなった)と判断され、メモリが解放される直前に、設定されたファイナライザ関数が実行されます。

ファイナライザは、ファイルハンドルやネットワーク接続などのOSリソースをクリーンアップするために使用されることがありますが、その実行タイミングはGCの動作に依存するため、予測が難しい場合があります。このコミットのケースでは、TestStackGrowthがスタック成長のテストの一環として、ファイナライザの実行を待機していることが示唆されます。

技術的詳細

このコミットは、Goランタイムのテストスイートの一部である TestStackGrowth の内部実装に焦点を当てています。このテストは、Goのスタック成長メカニズムが正しく機能するかどうかを検証するために設計されています。

テストの具体的な内容はコミットログからは読み取れませんが、select ステートメントと time.After を使用していることから、特定のイベント(この場合はファイナライザの実行)が一定時間内に完了することを期待していることがわかります。

元のコードでは、time.After(4 * time.Second) が使用されており、これはファイナライザが4秒以内に実行されることを期待していました。しかし、低速なビルド環境では、GCの実行サイクルやファイナライザのスケジューリングが遅延し、4秒というタイムアウトでは不十分であったため、テストが失敗していました。

この問題に対処するため、タイムアウト値が 20 * time.Second に変更されました。これにより、テストがファイナライザの実行を待つ時間が5倍に延長され、低速な環境でもテストが成功する可能性が高まります。

この変更は、Goランタイムの堅牢性を高めるための保守的な調整であり、ランタイムのコアロジック自体を変更するものではありません。しかし、テストの信頼性を向上させることで、将来的なランタイムの変更や最適化が、環境要因による誤ったテスト失敗によって妨げられることを防ぐ重要な意味を持ちます。

Goのテストは、CI/CDパイプラインの一部として様々な環境で実行されます。そのため、特定のハードウェア性能やOSのスケジューリング特性に依存して不安定になるテストは、開発プロセスを阻害する要因となります。このコミットは、そのような不安定性を排除し、テスト結果の信頼性を確保するための典型的な対応策と言えます。

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

変更は src/pkg/runtime/stack_test.go ファイルの1箇所のみです。

--- a/src/pkg/runtime/stack_test.go
+++ b/src/pkg/runtime/stack_test.go
@@ -162,7 +162,7 @@ func TestStackGrowth(t *testing.T) {
 		GC()
 		select {
 		case <-done:
-		case <-time.After(4 * time.Second):
+		case <-time.After(20 * time.Second):
 			t.Fatal("finalizer did not run")
 		}
 	}()

コアとなるコードの解説

変更された行は、Goの select ステートメント内で使用されている time.After 関数です。

select ステートメントは、複数の通信操作(チャネルの送受信)を待機するために使用されます。このコンテキストでは、以下の2つのケースを待機しています。

  1. <-done: done チャネルからの受信。これは、ファイナライザが正常に実行されたことを示すシグナルであると推測されます。
  2. <-time.After(X * time.Second): time.After 関数は、指定された期間(X秒)が経過した後に現在時刻を送信するチャネルを返します。このケースは、タイムアウトを実装するために使用されます。

元のコードでは、time.After(4 * time.Second) が使用されていました。これは、「ファイナライザが4秒以内に実行されなければ、タイムアウトとして処理する」という意味です。

変更後のコードでは、time.After(20 * time.Second) に変更されました。これにより、タイムアウトまでの時間が20秒に延長され、「ファイナライザが20秒以内に実行されなければ、タイムアウトとして処理する」という意味になります。

もし done チャネルからの受信がタイムアウトよりも早く発生すれば、テストは続行されます。しかし、タイムアウトが発生した場合(つまり、20秒以内にファイナライザが実行されなかった場合)、t.Fatal("finalizer did not run") が呼び出され、テストは失敗します。

この変更は、テストのロジック自体を変更するものではなく、テストが許容する実行時間の閾値を調整するものです。これにより、低速な環境でのテストの信頼性が向上します。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Goのガーベージコレクションについて: https://go.dev/doc/gc-guide
  • Goのスタックについて (Goのブログ記事など): https://go.dev/blog/go-stacks (これは一般的な情報源であり、特定の記事を指すものではありませんが、Goのスタックに関する公式情報を見つけるための良い出発点です)

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に runtime パッケージとテストファイル)
  • Go言語に関する技術ブログやフォーラムでの議論 (flaky tests, GC, finalizers, stack growthに関する一般的な情報)
  • time.After 関数のGoドキュメント: https://pkg.go.dev/time#After
  • select ステートメントのGo言語仕様: https://go.dev/ref/spec#Select_statements
  • Goのファイナライザに関するドキュメント: https://pkg.go.dev/runtime#SetFinalizer
  • Goのテストにおけるflaky testsに関する一般的な情報源 (例: GoのIssueトラッカーや開発者コミュニティの議論)