[インデックス 16325] ファイルの概要
このコミットは、Go言語の標準ライブラリである crypto/tls
パッケージ内のテストファイル src/pkg/crypto/tls/handshake_client_test.go
に関連するものです。このファイルは、TLSクライアントのハンドシェイク処理に関するテストケースを定義しており、特に TestEmptyRecords
というテスト関数が対象となっています。このテストは、TLS接続において空のレコードがどのように処理されるかを確認することを目的としています。
コミット
このコミットは、crypto/tls
パッケージ内の不安定な(flaky)テストを修正することを目的としています。具体的には、TestEmptyRecords
というテストが GOMAXPROCS
の値によって失敗することがあった問題を解決しています。この問題は、defer
ステートメントの実行順序が原因で、基盤となるネットワークパイプがTLSクライアント接続よりも先に閉じられてしまうことに起因していました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21cf646bfcc35711ce0c728f3d1e44ffe6b054e8
元コミット内容
crypto/tls: fix flakey test.
A test added in b37d2fdcc4d9 didn't work with some values of GOMAXPROCS
because the defer statements were in the wrong order: the Pipe could be
closed before the TLS Client was.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/9187047
変更の背景
b37d2fdcc4d9
で追加された TestEmptyRecords
というテストは、特定の GOMAXPROCS
の値(Goランタイムが同時に実行できるOSスレッドの最大数)において不安定に失敗するという問題(flaky test)を抱えていました。この不安定性は、テスト内で使用されている defer
ステートメントの実行順序が原因でした。具体的には、TLSクライアント接続を閉じる defer
が、その基盤となる net.Pipe
接続を閉じる defer
よりも先に実行される可能性があり、これによりテストが意図しないエラーを検出してしまうことがありました。
並行処理のタイミングに依存するこのような問題は、GOMAXPROCS
の値によってゴルーチンのスケジューリングが変わり、リソースの解放タイミングが変動するために発生します。このコミットは、この不安定性を解消し、テストが常に安定して動作するようにすることを目的としています。
前提知識の解説
Go言語の defer
ステートメント
Go言語の defer
ステートメントは、その関数がリターンする直前、またはパニックが発生して関数が終了する直前に、指定された関数呼び出しを延期(defer)するために使用されます。defer
された関数は、LIFO(Last-In, First-Out)の順序で実行されます。つまり、複数の defer
ステートメントがある場合、最後に defer
されたものが最初に実行され、最初に defer
されたものが最後に実行されます。これは、リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)を確実に行うための非常に便利な機能です。
GOMAXPROCS
環境変数
GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。デフォルトでは、利用可能なCPUコア数に設定されます。この値が変更されると、GoスケジューラがゴルーチンをOSスレッドにどのようにマッピングし、実行するかに影響を与えます。これにより、並行処理のタイミングが変わり、特定の競合状態やタイミング依存のバグが顕在化することがあります。不安定なテストが GOMAXPROCS
の値によって挙動を変えるのは、このスケジューリングの変化が原因であることが多いです。
net.Pipe()
関数
Go言語の net
パッケージには、Pipe()
という関数があります。これは、インメモリの双方向通信パイプを作成するために使用されます。Pipe()
は2つの net.Conn
インターフェースを実装するオブジェクトを返します。これらはそれぞれパイプの「両端」を表し、一方に書き込まれたデータはもう一方から読み取ることができます。この機能は、ネットワーク接続をシミュレートするテスト(特にクライアントとサーバー間の通信をテストする場合)で非常に有用です。実際のネットワークI/Oを伴わないため、テストを高速かつ決定論的に実行できます。
crypto/tls
パッケージ
crypto/tls
パッケージは、Go言語におけるTLS(Transport Layer Security)プロトコルの実装を提供します。TLSは、インターネット上での安全な通信を可能にするための暗号化プロトコルであり、ウェブブラウジング(HTTPS)、電子メール、その他の多くのアプリケーションで広く使用されています。このパッケージは、TLSクライアントとサーバーの実装、証明書の管理、ハンドシェイク処理など、TLS通信に必要な機能を提供します。
TLSハンドシェイク
TLSハンドシェイクは、TLS接続を確立するための初期プロトコルです。クライアントとサーバーが互いに認証し、暗号化アルゴリズムと共有秘密鍵をネゴシエートする一連のステップを含みます。ハンドシェイクが成功すると、その後の通信は確立された暗号化されたチャネルを通じて行われます。
Flaky Test(不安定なテスト)
Flaky Testとは、同じコードベースとテスト環境で実行しても、成功したり失敗したりするテストのことです。これは、テストが外部要因(ネットワークの遅延、データベースの状態、システム時刻など)や、並行処理におけるタイミングの競合、リソースリーク、不適切なテストのセットアップ/ティアダウンなどに依存している場合に発生します。Flaky Testは開発者の生産性を低下させ、CI/CDパイプラインの信頼性を損なうため、修正が強く推奨されます。
技術的詳細
このコミットの核心は、Goの defer
ステートメントの実行順序と、それがリソースのクリーンアップに与える影響を正しく理解し、適用することにあります。
元のコードでは、TestEmptyRecords
関数内のゴルーチンで、以下の defer
ステートメントがこの順序で記述されていました。
defer cli.Close() // TLSクライアント接続を閉じる
defer c.Close() // 基盤となるnet.Pipe接続を閉じる
Goの defer
はLIFO(Last-In, First-Out)で実行されるため、この順序では c.Close()
が cli.Close()
よりも先に実行されます。
cli
は tls.Conn
型であり、その内部で c
(net.Pipe
の一端)を使用しています。つまり、cli
は c
に依存しています。テストのシナリオでは、cli.Read(buf)
がブロックしている間に、GOMAXPROCS
の値やスケジューリングのタイミングによっては、ゴルーチンが終了し、defer
された関数が実行される可能性があります。
問題は、c.Close()
が cli.Close()
よりも先に実行されると、cli
がまだ c
を使用しようとしている間に、基盤となるパイプ接続 c
が閉じられてしまう可能性がある点にありました。これにより、cli.Read
が予期せぬエラーを返したり、cli
が不正な状態になったりして、テストが不安定に失敗することがありました。
このコミットでは、defer
ステートメントの順序を以下のように変更しました。
defer c.Close() // 基盤となるnet.Pipe接続を閉じる
defer cli.Close() // TLSクライアント接続を閉じる
この変更により、LIFOの原則に従って、cli.Close()
が c.Close()
よりも先に実行されるようになります。しかし、これは一見すると逆効果に見えます。
重要なのは、defer
の登録順序と実行順序です。
defer
は登録された順序とは逆(LIFO)に実行されます。
-
変更前:
defer cli.Close()
が登録される。defer c.Close()
が登録される。 実行時:c.Close()
->cli.Close()
-
変更後:
defer c.Close()
が登録される。defer cli.Close()
が登録される。 実行時:cli.Close()
->c.Close()
しかし、コミットメッセージと差分を再確認すると、私の理解が逆でした。
コミットメッセージには「the defer statements were in the wrong order: the Pipe could be closed before the TLS Client was.」とあります。
そして、差分は defer cli.Close()
と defer c.Close()
の行を入れ替えています。
元のコード:
defer cli.Close()
defer c.Close()
この場合、LIFOなので c.Close()
が先に実行され、次に cli.Close()
が実行されます。
コミットメッセージの「Pipe could be closed before the TLS Client was」と合致します。
修正後のコード:
defer c.Close()
defer cli.Close()
この場合、LIFOなので cli.Close()
が先に実行され、次に c.Close()
が実行されます。
これにより、「TLS Clientが閉じられた後にPipeが閉じられる」という正しい順序になります。
つまり、TLSクライアント接続 cli
が、その基盤となるパイプ接続 c
を使用し終えてから、または cli
自身が閉じられる際に c
を適切にクリーンアップできるように、cli.Close()
が c.Close()
よりも先に実行されるように順序が変更されたのです。これにより、cli
がまだ c
に依存している間に c
が閉じられるという競合状態が解消され、テストの不安定性が修正されました。
また、var config = *testConfig
から config := *testConfig
への変更は、Go言語における変数の短縮宣言(short variable declaration)への変更であり、機能的な意味合いは全くありません。これは単にGoの慣習に合わせたコードスタイルの変更です。
コアとなるコードの変更箇所
変更は src/pkg/crypto/tls/handshake_client_test.go
ファイルの TestEmptyRecords
関数内で行われました。
--- a/src/pkg/crypto/tls/handshake_client_test.go
+++ b/src/pkg/crypto/tls/handshake_client_test.go
@@ -84,7 +84,7 @@ func TestEmptyRecords(t *testing.T) {
// the first application data from the server. This test ensures that
// the empty record doesn't cause (0, nil) to be returned from
// Conn.Read.
- var config = *testConfig
+ config := *testConfig
config.CipherSuites = []uint16{TLS_RSA_WITH_AES_256_CBC_SHA}
c, s := net.Pipe()
@@ -92,8 +92,8 @@ func TestEmptyRecords(t *testing.T) {
go func() {
buf := make([]byte, 1024)
n, err := cli.Read(buf)
- defer cli.Close()
defer c.Close()
+ defer cli.Close()
if err != nil {
t.Fatalf("error reading from tls.Client: %s", err)
具体的には、以下の2点が変更されました。
var config = *testConfig
がconfig := *testConfig
に変更されました。(行86)defer cli.Close()
とdefer c.Close()
の順序が入れ替えられました。(行94-95)
コアとなるコードの解説
このコミットの主要な変更は、defer
ステートメントの順序の入れ替えです。
変更前:
defer cli.Close() // (1) 最初に登録される
defer c.Close() // (2) 次に登録される
この場合、LIFOの原則により、関数終了時には (2) c.Close()
が先に実行され、次に (1) cli.Close()
が実行されます。
これは、基盤となるパイプ接続 c
が、それを使用しているTLSクライアント接続 cli
よりも先に閉じられることを意味します。cli
がまだ c
を介したI/O操作(例: cli.Read
)を試みている間に c
が閉じられると、競合状態やエラーが発生し、テストが不安定になる原因となっていました。
変更後:
defer c.Close() // (1) 最初に登録される
defer cli.Close() // (2) 次に登録される
この場合、LIFOの原則により、関数終了時には (2) cli.Close()
が先に実行され、次に (1) c.Close()
が実行されます。
これにより、TLSクライアント接続 cli
が先に閉じられ、その後に基盤となるパイプ接続 c
が閉じられるという、依存関係を考慮した正しいクリーンアップ順序が保証されます。cli.Close()
が実行されることで、cli
は自身の内部リソースを適切に解放し、c
への依存を解消します。その後、c.Close()
が実行されて基盤のパイプが閉じられます。この順序により、テストの実行が安定し、GOMAXPROCS
の値に依存しない信頼性の高い結果が得られるようになりました。
var config = *testConfig
から config := *testConfig
への変更は、Go言語の慣用的な書き方への修正であり、機能的な影響はありません。:=
は変数の宣言と初期化を同時に行う短縮宣言で、型推論が行われます。
関連リンク
- Go CL 9187047: https://golang.org/cl/9187047
- 関連する元のコミット
b37d2fdcc4d9
: https://github.com/golang/go/commit/b37d2fdcc4d9
参考にした情報源リンク
- Go言語の
defer
ステートメントに関する公式ドキュメント: https://go.dev/blog/defer-panic-and-recover - Go言語の
GOMAXPROCS
に関する情報: https://pkg.go.dev/runtime#GOMAXPROCS net.Pipe()
関数のドキュメント: https://pkg.go.dev/net#Pipecrypto/tls
パッケージのドキュメント: https://pkg.go.dev/crypto/tls- Flaky Testに関する一般的な情報 (例: Martin Fowler's blog): https://martinfowler.com/articles/flakyTests.htmlI have generated the commit explanation in Markdown format, following all the specified instructions and chapter structure. The output has been sent to standard output only.