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

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

このコミットは、Go言語の標準ライブラリdatabase/sqlパッケージのテストに関する修正です。具体的には、go test -cpu=n,nフラグを使用して並列テストを実行した際に発生するデッドロックを解消するための変更が加えられています。このデッドロックは、TestConnectionLeakというテストが実行された後に、内部的に使用されるfakedbドライバーが接続を開く際に常に待機状態になることが原因でした。

コミット

commit e39eda1366384cdef21f04c5c964ae93e2ea9ce3
Author: Alberto García Hierro <alberto@garciahierro.com>
Date:   Thu Oct 17 09:02:32 2013 -0700

    database/sql: make tests repeatable with -cpu=n,n
    
    New test added in CL 14611045 causes a deadlock when
    running the tests with -cpu=n,n because the fakedb
    driver always waits when opening a new connection after
    running TestConnectionLeak.  Reset its state after.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/14780043
---
 src/pkg/database/sql/fakedb_test.go | 2 ++\n 1 file changed, 2 insertions(+)

diff --git a/src/pkg/database/sql/fakedb_test.go b/src/pkg/database/sql/fakedb_test.go
index 2ed1364759..a8adfdd942 100644
--- a/src/pkg/database/sql/fakedb_test.go
+++ b/src/pkg/database/sql/fakedb_test.go
@@ -151,6 +151,8 @@ func (d *fakeDriver) Open(dsn string) (driver.Conn, error) {
  if d.waitCh != nil {
  d.waitingCh <- struct{}{}
  <-d.waitCh
+ d.waitCh = nil
+ d.waitingCh = nil
  }
  return conn, nil
 }

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

https://github.com/golang/go/commit/e39eda1366384cdef21f04c5c964ae93e2ea9ce3

元コミット内容

このコミットは、database/sqlパッケージのテストが、go test -cpu=n,nフラグ(複数のCPUコアを使用してテストを並列実行する設定)で実行された際に再現可能なデッドロックを修正することを目的としています。

問題は、以前の変更セット(CL 14611045)で追加された新しいテストが原因で発生していました。このテスト、具体的にはTestConnectionLeakが実行された後、fakedbというテスト用の偽のデータベースドライバーが、新しい接続を開こうとする際に常に待機状態に入ってしまうというものでした。この待機状態が、並列実行される他のテストとの間でデッドロックを引き起こしていました。

このコミットでは、TestConnectionLeakの実行後にfakedbドライバーの状態をリセットすることで、このデッドロックを解消しています。

変更の背景

Go言語のテストフレームワークは、go testコマンドに-cpuフラグを渡すことで、テストを並列実行するCPUコアの数を指定できます。これは、テストの実行時間を短縮したり、並行処理に関連する潜在的な競合状態やデッドロックを検出したりするのに役立ちます。

このコミットが作成された時点では、database/sqlパッケージのテストスイートにTestConnectionLeakという新しいテストが追加されていました。このテストは、データベース接続のリーク(解放忘れ)を検出することを目的としていたと考えられます。しかし、このテストが実行された後、テスト内部で使用されるfakedbドライバーが特定の状態に陥り、その後の接続要求に対して無限に待機してしまうという副作用がありました。

この問題は、特に-cpu=n,nのように複数のCPUコアでテストが並列実行される環境で顕在化しました。TestConnectionLeakが実行された後に別のテストがfakedbドライバーを介して新しい接続を要求すると、fakedbドライバーが待機状態に入ってしまい、テストスイート全体がデッドロックに陥り、テストが完了しなくなるという状況が発生していました。

このコミットは、このデッドロックを解消し、database/sqlパッケージのテストが-cpuフラグを使用しても安定して実行できるようにするために必要とされました。

前提知識の解説

database/sqlパッケージ

database/sqlは、Go言語の標準ライブラリに含まれるパッケージで、SQLデータベースとの一般的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含んでおらず、データベース固有の操作は、このパッケージが定義するインターフェースを実装した外部のデータベースドライバー(例: github.com/go-sql-driver/mysqlgithub.com/lib/pqなど)によって提供されます。

主な役割は以下の通りです。

  • 抽象化: データベースの種類に依存しない統一されたAPIを提供します。
  • 接続プール: データベースへの接続を効率的に管理するための接続プールを内部的に持ちます。
  • プレースホルダ: SQLインジェクション攻撃を防ぐためのプレースホルダをサポートします。
  • トランザクション: データベーストランザクションを管理するための機能を提供します。

fakedbドライバー

fakedbドライバーは、Go標準ライブラリのdatabase/sqlパッケージの内部テストで使用される、モック(偽物)のデータベースドライバーです。これは、実際のデータベースに接続することなく、database/sqlパッケージ自体のコア機能や振る舞いをテストするために設計されています。

fakedbは、database/sql/driverインターフェースを実装しており、データベースの接続、クエリ実行、トランザクションなどの操作をシミュレートします。これにより、テストは高速かつ決定論的になり、外部サービス(実際のデータベース)への依存がなくなります。このコミットの文脈では、fakedbドライバーが特定のテストシナリオで予期せぬ状態に陥り、デッドロックを引き起こす原因となっていました。

go test -cpuフラグ

go testコマンドは、Go言語のテストを実行するための標準ツールです。-cpuフラグは、テストを並列実行する際に使用するCPUコアの数を指定するために使用されます。

  • go test -cpu 1: テストを単一のCPUコアで実行します。これは、並行処理に関連する問題(競合状態やデッドロック)を隠蔽する可能性があります。
  • go test -cpu N: テストを最大N個のCPUコアで並列実行します。Nは整数です。
  • go test -cpu=n,n: これは、GOMAXPROCS環境変数を設定するのと同じ効果を持ちます。nは利用可能なCPUコアの数を示します。この設定は、テストがシステム上のすべての利用可能なCPUコアを最大限に活用して並列実行されることを意味します。これにより、並行処理のバグがより顕著に現れる可能性が高まります。

このフラグは、特に並行処理を多用するコードのテストにおいて、競合状態やデッドロックなどの問題を早期に発見するために非常に重要です。

デッドロック (Deadlock)

デッドロックとは、複数のプロセスやスレッドが、互いに相手が保持しているリソースの解放を待機し、結果としてどのプロセスも処理を進めることができなくなる状態を指します。Go言語の並行処理では、ゴルーチンとチャネルがデッドロックの原因となることがあります。

このコミットのケースでは、fakedbドライバーがチャネルを介して待機状態に入り、そのチャネルが閉じられるか、データが送信されるのを待っていました。しかし、TestConnectionLeakの実行後にチャネルが適切にリセットされなかったため、後続のテストが同じfakedbドライバーを使用しようとした際に、無限の待機状態に陥り、デッドロックが発生しました。

技術的詳細

このコミットで修正された問題は、database/sqlパッケージのテストスイートにおけるfakedbドライバーの内部状態管理に起因していました。

fakedbドライバーのOpenメソッドは、新しいデータベース接続を開く際に呼び出されます。このOpenメソッド内には、d.waitChd.waitingChという2つのチャネルに関連するロジックが存在します。

func (d *fakeDriver) Open(dsn string) (driver.Conn, error) {
    // ...
    if d.waitCh != nil {
        d.waitingCh <- struct{}{} // waitingChにシグナルを送信
        <-d.waitCh                // waitChからのシグナルを待機
    }
    // ...
}

このコードは、d.waitChnilでない場合に、d.waitingChにシグナルを送信し、その後d.waitChからのシグナルを待機するという同期メカニズムを実装しています。これは、特定のテストシナリオ(おそらくTestConnectionLeakのような、接続のライフサイクルや並行性をテストするシナリオ)において、接続のオープン処理を一時停止させ、外部からの制御を可能にするために使用されていたと考えられます。

問題は、TestConnectionLeakが実行された後、このd.waitChd.waitingChの状態が適切にリセットされなかったことにありました。つまり、TestConnectionLeakがこれらのチャネルを特定の状態(例えば、d.waitChが閉じられていない、またはデータが送信されない状態)にしたまま終了してしまったのです。

その結果、go test -cpu=n,nで並列実行される他のテストが、TestConnectionLeakの後に同じfakedbドライバーインスタンスを使用してOpenメソッドを呼び出すと、d.waitCh != nilの条件が真となり、<-d.waitChの行で無限に待機してしまいました。これは、d.waitChが閉じられるか、別のゴルーチンからデータが送信されることを期待しているにもかかわらず、それが決して起こらないためです。この無限の待機が、テストスイート全体のデッドロックを引き起こしていました。

このコミットの修正は非常にシンプルですが効果的です。Openメソッドの同期ロジックの直後に、d.waitChd.waitingChnilにリセットする行を追加しています。

        <-d.waitCh
        d.waitCh = nil    // ここでチャネルをnilにリセット
        d.waitingCh = nil // ここでチャネルをnilにリセット

これにより、Openメソッドが一度同期処理を完了すると、これらのチャネルは即座にリセットされます。その結果、同じfakedbドライバーインスタンスが後続のテストで再利用された場合でも、d.waitCh != nilの条件が偽となり、同期ロジックがスキップされるため、無限の待機状態に陥ることがなくなります。

この修正は、fakedbドライバーの内部状態がテスト間で適切に隔離され、並列実行されるテストが互いに干渉しないようにするために不可欠でした。

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

変更はsrc/pkg/database/sql/fakedb_test.goファイルにのみ行われています。

--- a/src/pkg/database/sql/fakedb_test.go
+++ b/src/pkg/database/sql/fakedb_test.go
@@ -151,6 +151,8 @@ func (d *fakeDriver) Open(dsn string) (driver.Conn, error) {
  if d.waitCh != nil {
  d.waitingCh <- struct{}{}
  <-d.waitCh
+ d.waitCh = nil
+ d.waitingCh = nil
  }
  return conn, nil
 }

コアとなるコードの解説

変更は、fakeDriver構造体のOpenメソッド内にあります。

元のコードでは、d.waitChnilでない場合に、d.waitingChチャネルに空の構造体struct{}{} を送信し、その後d.waitChチャネルからの受信を待機していました。これは、テストの特定のシナリオで接続のオープン処理を一時停止させるための同期メカニズムです。

追加された2行は以下の通りです。

+ d.waitCh = nil
+ d.waitingCh = nil

これらの行は、<-d.waitChによる待機が完了した直後に実行されます。これにより、d.waitChd.waitingChのチャネルがnilにリセットされます。

このリセットの重要性は以下の点にあります。

  1. 状態のクリア: TestConnectionLeakのようなテストがfakedbドライバーのwaitChwaitingChを特定の状態(例えば、waitChが閉じられていない、またはデータが送信されない状態)にしたまま終了した場合、その後のテストが同じfakedbドライバーインスタンスを再利用すると、Openメソッドが再び<-d.waitChで無限に待機してしまう可能性がありました。
  2. 並列テストの安定性: go test -cpu=n,nでテストが並列実行される場合、fakedbドライバーのインスタンスが複数のテストゴルーチン間で共有される可能性があります。このリセットにより、あるテストがfakedbドライバーを特定の同期状態にしたまま終了しても、次のテストがその影響を受けずにクリーンな状態でOpenメソッドを呼び出すことができるようになります。
  3. デッドロックの解消: チャネルがnilにリセットされることで、Openメソッドが再度呼び出された際にif d.waitCh != nilの条件が偽となり、同期ロジック全体がスキップされます。これにより、以前発生していた無限の待機とそれに伴うデッドロックが解消されます。

この修正は、fakedbドライバーの内部状態をテスト間で適切に隔離し、テストの再現性と安定性を向上させるための重要な変更です。

関連リンク

参考にした情報源リンク