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

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

このコミットは、Go言語の実験的なexp/sqlパッケージ(現在のdatabase/sqlパッケージの前身)における、ミューテックスのアンロック漏れによって引き起こされるデッドロックのバグを修正するものです。具体的には、データベース接続が既に閉じられているエラーケースにおいて、ミューテックスが適切に解放されないために発生する競合状態を解消します。この修正により、データベースが閉じられた後にDB.Queryなどの操作が呼び出された際に、システムがハングアップするのを防ぎます。

コミット

commit 06a9bc683518552991820581cb8a4cf5e6978d47
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Dec 12 13:56:56 2011 -0800

    sql: fix missing mutex unlock in an error case
    
    Fixes #2542
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5483054

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

https://github.com/golang/go/commit/06a9bc683518552991820581cb8a4cf5e6978d47

元コミット内容

このコミットの目的は、「エラーケースにおけるミューテックスのアンロック漏れを修正する」ことです。これは、Go言語のIssue 2542で報告された問題に対応しています。具体的には、exp/sqlパッケージのDB.conn()メソッド内で、データベースが既に閉じられている場合にミューテックスがロックされたままになり、その後の操作でデッドロックが発生する可能性があった問題を解決します。

変更の背景

この変更は、Go言語のIssue 2542「exp/sql: deadlock when querying on a closed connection」を修正するために行われました。この問題は、exp/sqlパッケージを使用しているアプリケーションにおいて、データベース接続が既に閉じられている状態(例えば、db.Close()が呼び出された後)で、さらにdb.Query()などの操作を試みた場合に発生する可能性がありました。

根本原因は、DB.conn()メソッド内でミューテックス(db.mu)がロックされるものの、データベースが閉じられているというエラーパスに入った際に、このミューテックスが解放されないままで関数を抜けてしまうことにありました。これにより、同じDBインスタンスに対して別のゴルーチンが接続を試みようとすると、既にロックされているミューテックスを取得しようとして永久に待機状態に入り、結果としてデッドロックが発生していました。

このようなデッドロックは、特に高負荷なシステムや、エラーハンドリングが不十分な場合に顕在化しやすく、アプリケーション全体の応答停止を引き起こす重大なバグとなります。このコミットは、この特定のデッドロックシナリオを解消し、exp/sqlパッケージの堅牢性を向上させることを目的としています。

前提知識の解説

1. ミューテックス (Mutex) と排他制御

ミューテックス(Mutual Exclusion、相互排他)は、並行プログラミングにおいて共有リソースへのアクセスを制御するための同期プリミティブです。複数のゴルーチン(またはスレッド)が同時に同じデータにアクセスしようとすると、データの整合性が損なわれる可能性があります(競合状態)。これを防ぐために、ミューテックスは一度に一つのゴルーチンだけが共有リソースにアクセスできるようにします。

  • ロック (Lock): ゴルーチンが共有リソースにアクセスする前に、ミューテックスをロックします。ロックが成功すると、そのゴルーチンがリソースへの排他的アクセス権を得ます。他のゴルーチンが同じミューテックスをロックしようとすると、ロックが解放されるまで待機します。
  • アンロック (Unlock): ゴルーチンが共有リソースへのアクセスを終えたら、ミューテックスをアンロックします。これにより、他の待機中のゴルーチンがロックを取得できるようになります。

ミューテックスのロックとアンロックは常にペアで行われる必要があります。ロックされたミューテックスがアンロックされないままになると、そのミューテックスを待機している他のゴルーチンは永久にブロックされ、デッドロックの原因となります。

2. デッドロック (Deadlock)

デッドロックは、複数のプロセスやゴルーチンが互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態を指します。ミューテックスの文脈では、以下のようなシナリオで発生します。

  • ミューテックスのアンロック漏れ: あるゴルーチンがミューテックスをロックしたが、エラー発生などの理由でアンロックせずに終了してしまった場合、そのミューテックスは永久にロックされたままになります。他のゴルーチンがそのミューテックスをロックしようとすると、永久に待機することになり、デッドロックが発生します。
  • 循環的待機: 複数のミューテックスがあり、ゴルーチンAがミューテックスXをロックし、ミューテックスYを待機している間に、ゴルーチンBがミューテックスYをロックし、ミューテックスXを待機しているような状況です。

3. Go言語のsync.Mutex

Go言語では、syncパッケージにミューテックスの実装であるsync.Mutexが提供されています。

import "sync"

var mu sync.Mutex

func someFunction() {
    mu.Lock()   // ロック
    defer mu.Unlock() // 関数終了時に必ずアンロックされるようにdeferを使うのが一般的
    // 共有リソースへのアクセス
}

defer mu.Unlock()は、関数がリターンする直前にmu.Unlock()が実行されることを保証するGoの便利な機能です。これにより、関数の途中でエラーが発生した場合でも、ミューテックスが確実に解放されるようにすることができます。しかし、このコミットの対象となったコードでは、deferが使われていなかったため、特定のエラーパスでアンロックが漏れていました。

4. exp/sqlパッケージ (現在のdatabase/sql)

exp/sqlは、Go言語の標準ライブラリであるdatabase/sqlパッケージの初期の実験的なバージョンです。このパッケージは、GoアプリケーションからSQLデータベースと対話するための汎用的なインターフェースを提供します。データベースドライバーは、このインターフェースを実装することで、特定のデータベース(PostgreSQL, MySQL, SQLiteなど)への接続を可能にします。

database/sqlパッケージは、データベース接続のプール管理、トランザクション管理、クエリの実行などを抽象化し、開発者がデータベース操作をより簡単に行えるように設計されています。内部的には、複数のゴルーチンからの同時アクセスを安全に処理するために、ミューテックスなどの同期プリミティブが多用されています。

技術的詳細

このコミットが修正する問題は、src/pkg/exp/sql/sql.goファイル内のDB.conn()メソッドに存在していました。このメソッドは、データベース接続プールから利用可能な接続を取得するか、新しい接続を作成する役割を担っています。

DB.conn()メソッドの冒頭で、db.mu.Lock()が呼び出され、DB構造体に関連付けられたミューテックスがロックされます。これは、接続プール(db.freeConn)やデータベースの状態(db.closed)といった共有リソースへのアクセスを排他的に行うためです。

問題のコードは以下の部分でした(修正前を想定):

func (db *DB) conn() (driver.Conn, error) {
    db.mu.Lock()
    if db.closed {
        // ここでdb.mu.Unlock()がなかった
        return nil, errors.New("sql: database is closed")
    }
    // ... 正常系の処理 ...
}

db.closedtrueの場合、つまりデータベースが既に閉じられている場合、メソッドはエラーを返して終了します。しかし、このエラーパスではdb.mu.Unlock()が呼び出されていませんでした。その結果、db.muミューテックスはロックされたままになり、DB.conn()を呼び出したゴルーチンが終了しても、ミューテックスは解放されません。

この状態が続くと、同じDBインスタンスに対して後続のDB.conn()呼び出し(例えば、db.Query()内部から)が行われた際に、db.mu.Lock()で永久にブロックされてしまいます。これは、ミューテックスが既にロックされており、誰もそれを解放しないためです。これがデッドロックの直接的な原因でした。

修正は、この特定のエラーパスにdb.mu.Unlock()を追加することで、ミューテックスが常に解放されるように保証します。これにより、データベースが閉じられているというエラーが発生した場合でも、ミューテックスが適切に解放され、他のゴルーチンがブロックされることなく、エラーを適切に処理できるようになります。

また、この修正を検証するために、src/pkg/exp/sql/sql_test.goに新しいテストケースTestIssue2542Deadlockが追加されました。このテストは、意図的にデータベースを閉じた後、複数回クエリを実行し、デッドロックが発生しないこと、そして期待通りにエラーが返されることを確認します。これにより、修正が正しく機能していることが保証されます。

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

src/pkg/exp/sql/sql.go

--- a/src/pkg/exp/sql/sql.go
+++ b/src/pkg/exp/sql/sql.go
@@ -134,6 +134,7 @@ func (db *DB) maxIdleConns() int {
 func (db *DB) conn() (driver.Conn, error) {
 	db.mu.Lock()
 	if db.closed {
+		db.mu.Unlock()
 		return nil, errors.New("sql: database is closed")
 	}
 	if n := len(db.freeConn); n > 0 {

src/pkg/exp/sql/sql_test.go

--- a/src/pkg/exp/sql/sql_test.go
+++ b/src/pkg/exp/sql/sql_test.go
@@ -228,3 +228,16 @@ func TestTxStmt(t *testing.T) {
 		t.Fatalf("Commit = %v", err)
 	}
 }
+
+// Tests fix for issue 2542, that we release a lock when querying on
+// a closed connection.
+func TestIssue2542Deadlock(t *testing.T) {
+	db := newTestDB(t, "people")
+	closeDB(t, db)
+	for i := 0; i < 2; i++ {
+		_, err := db.Query("SELECT|people|age,name|")
+		if err == nil {
+			t.Fatalf("expected error")
+		}
+	}
+}

コアとなるコードの解説

src/pkg/exp/sql/sql.go の変更

DB.conn()メソッドは、データベース接続を取得する内部関数です。このメソッドの冒頭でdb.mu.Lock()が呼び出され、DB構造体のミューテックスをロックします。

変更前は、if db.closedの条件が真(データベースが閉じられている)の場合、エラーを返して関数を終了していましたが、db.mu.Unlock()が呼び出されていませんでした。これにより、ミューテックスがロックされたままになり、後続の操作がデッドロックを引き起こす可能性がありました。

追加されたdb.mu.Unlock()は、このエラーパスにおいてミューテックスを適切に解放することを保証します。これにより、データベースが閉じられているというエラーが発生しても、ミューテックスが解放され、他のゴルーチンがdb.mu.Lock()でブロックされることなく、エラーを処理できるようになります。これは、ミューテックスのロックとアンロックが常にペアで行われるべきであるという原則に従った、デッドロック回避のための重要な修正です。

src/pkg/exp/sql/sql_test.go の変更

TestIssue2542Deadlockという新しいテスト関数が追加されました。このテストの目的は、Issue 2542で報告されたデッドロックの問題が修正されたことを検証することです。

  1. db := newTestDB(t, "people"): テスト用のデータベース接続を作成します。
  2. closeDB(t, db): 作成したデータベース接続を意図的に閉じます。
  3. for i := 0; i < 2; i++: データベースが閉じられた状態で、db.Query()を複数回(ここでは2回)呼び出します。
  4. _, err := db.Query("SELECT|people|age,name|"): クエリを実行します。データベースが閉じられているため、この呼び出しはエラーを返すことが期待されます。
  5. if err == nil { t.Fatalf("expected error") }: もしエラーが返されなかった場合、それは予期せぬ動作であり、テストは失敗します。

このテストは、データベースが閉じられた後にdb.Query()を呼び出しても、デッドロックが発生せず、代わりに適切なエラーが返されることを確認します。これにより、修正が正しく機能し、システムが安定していることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語 syncパッケージのドキュメント: https://pkg.go.dev/sync
  • Go言語 database/sqlパッケージのドキュメント: https://pkg.go.dev/database/sql
  • 並行プログラミングにおけるデッドロックの概念に関する一般的な情報源。