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

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

コミット

commit f28c8fba67008ae0b14517979e9b48ac38fa22b6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 14 15:01:45 2013 -0700

    database/sql: associate a mutex with each driver interface
    
    The database/sql/driver docs make this promise:
    
       "Conn is a connection to a database. It is not used
       concurrently by multiple goroutines."
    
    That promises exists as part of database/sql's overall
    goal of making drivers relatively easy to write.
    
    So far this promise has been kept without the use of locks by
    being careful in the database/sql package, but sometimes too
    careful. (cf. golang.org/issue/3857)
    
    The CL associates a Mutex with each driver.Conn, and with the
    interface value progeny thereof. (e.g. each driver.Tx,
    driver.Stmt, driver.Rows, driver.Result, etc) Then whenever
    those interface values are used, the Locker is locked.
    
    This CL should be a no-op (aside from some new Lock/Unlock
    pairs) and doesn't attempt to fix Issue 3857 or Issue 4459,
    but should make it much easier in a subsequent CL.
    
    Update #3857
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/7803043

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

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

元コミット内容

database/sql: 各ドライバインターフェースにミューテックスを関連付ける

database/sql/driver のドキュメントには以下の約束があります:

Conn はデータベースへの接続です。複数のゴルーチンによって同時に使用されることはありません。」

この約束は、ドライバを比較的簡単に記述できるようにするという database/sql の全体的な目標の一部として存在します。

これまでこの約束は、database/sql パッケージ内で注意深く実装することでロックを使用せずに守られてきましたが、時には注意しすぎることもありました。(参照: golang.org/issue/3857)

この変更リスト (CL) は、各 driver.Conn およびその派生インターフェース値 (例: 各 driver.Tx, driver.Stmt, driver.Rows, driver.Result など) に Mutex を関連付けます。これらのインターフェース値が使用されるたびに、Locker がロックされます。

この CL は (いくつかの新しい Lock/Unlock ペアを除いて) 何も変更しないはずであり、Issue 3857 や Issue 4459 を直接修正しようとはしていませんが、後続の CL での修正をはるかに容易にするはずです。

Issue #3857 を更新

R=golang-dev, adg CC=golang-dev https://golang.org/cl/7803043

変更の背景

Go言語の database/sql パッケージは、データベースドライバとアプリケーションコードの間の抽象化レイヤーを提供します。このパッケージの設計原則の一つに、ドライバの実装を簡素化するために、driver.Conn インターフェースが複数のゴルーチンによって同時に使用されないという保証があります。しかし、この保証を database/sql パッケージ側で厳密に守ろうとすると、過剰な注意や複雑なロジックが必要になる場合がありました。

特に、golang.org/issue/3857 では、database/sql パッケージが driver.Conn の非同時使用の約束をどのように扱うかについて議論されていました。この問題は、database/sql が内部的に接続をプールし、複数の操作で同じ接続を再利用する際に、ドライバの実装がこの非同時使用の約束に依存している場合に、予期せぬ競合状態やデッドロックが発生する可能性を示唆していました。

このコミットの目的は、database/sql パッケージがドライバインターフェースの非同時使用の約束をより堅牢に、かつシンプルに強制できるようにするための基盤を構築することです。具体的には、各ドライバインターフェース値(Connだけでなく、TxStmtRowsResultなど、Connから派生するすべてのインターフェース)に明示的にミューテックスを関連付けることで、これらのインターフェースへのアクセスを同期させ、ドライバが非同時使用の約束に安心して依存できるようにします。

この変更自体は、既存のバグ(Issue 3857やIssue 4459)を直接修正するものではありませんが、これらの問題を解決するための後続の変更を容易にすることを意図しています。これにより、database/sql パッケージの内部実装が簡素化され、将来的な改善やバグ修正がより安全かつ効率的に行えるようになります。

前提知識の解説

Go言語の database/sql パッケージ

Go言語の標準ライブラリに含まれる database/sql パッケージは、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作は database/sql/driver パッケージで定義されたインターフェースを実装する外部ドライバに委譲されます。

database/sql/driver パッケージ

database/sql/driver パッケージは、データベースドライバが実装すべきインターフェースを定義しています。主要なインターフェースには以下のようなものがあります。

  • driver.Driver: データベースへの接続を開くためのインターフェース。
  • driver.Conn: データベースへの単一の接続を表すインターフェース。このインターフェースのドキュメントには「複数のゴルーチンによって同時に使用されない」という重要な約束があります。
  • driver.Tx: トランザクションを表すインターフェース。
  • driver.Stmt: プリペアドステートメントを表すインターフェース。
  • driver.Rows: クエリ結果の行を表すインターフェース。
  • driver.Result: Exec 操作の結果を表すインターフェース。

ゴルーチンと並行性

Go言語は軽量なスレッドであるゴルーチン(goroutine)とチャネル(channel)を用いた並行処理をサポートしています。複数のゴルーチンが同時に実行される可能性があるため、共有リソースへのアクセスを適切に同期しないと、競合状態(race condition)が発生し、プログラムの動作が予測不能になったり、データが破損したりする可能性があります。

ミューテックス (sync.Mutex)

ミューテックス(Mutex)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。Go言語の sync パッケージには sync.Mutex が提供されており、Lock() メソッドでロックを取得し、Unlock() メソッドでロックを解放します。これにより、一度に一つのゴルーチンのみが保護されたコードセクション(クリティカルセクション)を実行することを保証し、競合状態を防ぎます。

インターフェースと埋め込み

Go言語のインターフェースは、メソッドのセットを定義します。構造体は、そのインターフェースのすべてのメソッドを実装することで、そのインターフェースを満たすことができます。また、Goの構造体は他の構造体やインターフェースを「埋め込む」ことができます。これにより、埋め込まれた型のメソッドが外側の型に「昇格」され、あたかも外側の型自身のメソッドであるかのように呼び出すことができます。このコミットでは、sync.Mutex を構造体に埋め込むことで、その構造体が sync.Locker インターフェースを満たすようにしています。

golang.org/issue/3857

このIssueは、database/sql パッケージにおける接続のライフサイクルと並行性に関する問題点を指摘しています。特に、driver.Conn が非同時使用であるという約束にもかかわらず、database/sql パッケージが内部的に接続を再利用する際に、ドライバの実装がこの約束に違反しているかのように見える状況が発生していました。このコミットは、この問題の根本的な解決策ではなく、その解決を容易にするための準備段階と位置づけられています。

技術的詳細

このコミットの核心は、database/sql パッケージが driver.Conn インターフェースとその派生インターフェース(driver.Tx, driver.Stmt, driver.Rows, driver.Result)へのアクセスを同期させるための新しいメカニズムを導入することです。

これまでの database/sql パッケージは、driver.Conn が非同時使用であるという約束を、内部的なロジックで「注意深く」守ろうとしていました。しかし、これは複雑さを増し、golang.org/issue/3857 のような問題を引き起こす可能性がありました。

このコミットでは、以下の新しい型が導入されます。

  1. driverConn 構造体:

    type driverConn struct {
        sync.Mutex
        ci driver.Conn
    }
    

    この構造体は、実際の driver.Conn インターフェース (ci) と sync.Mutex を埋め込んでいます。これにより、driverConn のインスタンスは sync.Locker インターフェース(Lock()Unlock() メソッドを持つ)を満たします。database/sql パッケージは、データベースドライバから取得した生の driver.Conn を直接使用する代わりに、この driverConn でラップして使用するようになります。これにより、driver.Conn へのすべての操作の前に Lock() を呼び出し、操作後に Unlock() を呼び出すことで、driver.Conn へのアクセスを排他的に制御できるようになります。

  2. driverStmt 構造体:

    type driverStmt struct {
        sync.Locker // the *driverConn
        si          driver.Stmt
    }
    

    この構造体は、driver.Stmt インターフェース (si) と、その driver.Stmt が生成された元の driverConnsync.Locker インターフェースを埋め込んでいます。これにより、driver.Stmt への操作も、元の接続のミューテックスによって保護されるようになります。同様に、driver.Txdriver.Rowsdriver.Result なども、対応する driverConn のミューテックスによって保護されるように変更されています。

変更のメカニズム:

  • DB 構造体の変更: DB 構造体内の接続プール (outConn, freeConn, onConnPut, lastPut) が、driver.Conn の代わりに新しく定義された *driverConn を保持するように変更されます。
  • 接続の取得と解放: db.conn() メソッドは、生の driver.Conn を開いた後、それを driverConn でラップして返します。db.putConn() メソッドも *driverConn を受け取るように変更されます。
  • インターフェース操作の同期: Exec, Query, Prepare, Begin, Commit, Rollback などの操作を行う際に、対応する driverConnLock()Unlock() が呼び出されるようになります。これにより、driver.Conn およびその派生インターフェースへのすべての呼び出しが、ミューテックスによって保護されます。
  • withLock ヘルパー関数: 新しいヘルパー関数 withLock が導入され、ミューテックスのロックとアンロックを簡潔に記述できるようになります。
    func withLock(lk sync.Locker, fn func()) {
        lk.Lock()
        fn()
        lk.Unlock()
    }
    

この変更により、database/sql パッケージは、ドライバが driver.Conn の非同時使用の約束を破った場合でも、内部的に安全性を確保できるようになります。これは、ドライバの実装がこの約束に厳密に従っているかどうかに関わらず、database/sql パッケージが堅牢に動作するための重要なステップです。

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

主に src/pkg/database/sql/sql.gosrc/pkg/database/sql/convert.go、そしてテストファイルである src/pkg/database/sql/sql_test.go が変更されています。

src/pkg/database/sql/sql.go

  • DB 構造体の outConn, freeConn, onConnPut, lastPut フィールドの型が driver.Conn から *driverConn に変更されました。
  • 新しい構造体 driverConndriverStmt が定義されました。
    • driverConnsync.Mutexdriver.Conn を持ちます。
    • driverStmtsync.Lockerdriver.Stmt を持ちます。
  • db.conn() メソッドが *driverConn を返すように変更され、内部で driver.ConndriverConn でラップするようになりました。
  • db.putConn() メソッドが *driverConn を受け取るように変更されました。
  • db.Close() メソッドで、freeConn 内の接続を閉じる際に dc.Lock()dc.Unlock() が追加されました。
  • db.prepare(), db.exec(), db.queryConn(), db.begin() などのメソッドで、driver.Conn の代わりに *driverConn を使用し、driver.Conn インターフェースのメソッド呼び出しの前後で Lock()Unlock() が追加されました。
  • Tx 構造体の ci フィールドが driver.Conn から *driverConn に変更され、Commit()Rollback() メソッドに tx.dc.Lock()tx.dc.Unlock() が追加されました。
  • Stmt 構造体の connStmt フィールドが driver.Conn から *driverConn に変更され、txsi フィールドが driver.Stmt から *driverStmt に変更されました。
  • resultFromStatement 関数が driver.Stmt の代わりに driverStmt を受け取るように変更され、Exec 呼び出しの前後で Lock()Unlock() が追加されました。
  • rowsiFromStatement 関数が driver.Stmt の代わりに driverStmt を受け取るように変更され、Query 呼び出しの前後で Lock()Unlock() が追加されました。
  • Rows 構造体の ci フィールドが driver.Conn から *driverConn に変更されました。
  • 新しいヘルパー関数 withLock が追加されました。

src/pkg/database/sql/convert.go

  • driverArgs 関数が driver.Stmt の代わりに *driverStmt を受け取るように変更されました。
  • ColumnConverterConvertValue メソッド呼び出しの前後で ds.Lock()ds.Unlock() が追加されました。

src/pkg/database/sql/sql_test.go

  • テストヘルパー関数 putConnHook の引数が driver.Conn から *driverConn に変更されました。
  • テスト内の db.freeConn[0] の型アサーションが *fakeConn から ci.(*fakeConn) に変更され、driverConn の内部の driver.Conn にアクセスするように修正されました。

コアとなるコードの解説

このコミットの主要な変更は、database/sql パッケージがドライバインターフェースへのアクセスをどのように同期するかという点にあります。

  1. driverConndriverStmt の導入:

    • driverConn は、実際の driver.Conn インターフェースを sync.Mutex と共にラップします。これにより、database/sql パッケージは、ドライバの Conn インターフェースへのすべての呼び出しを、この Mutex を使って保護できるようになります。
    • driverStmt は、driver.Stmt インターフェースと、その Stmt が属する driverConnsync.Locker を持ちます。これにより、Stmt への操作も、元の接続のミューテックスによって保護されます。同様のラッパーが Tx, Rows, Result にも暗黙的に適用されます。
  2. 接続プールの変更: DB 構造体内の接続プール(outConn, freeConn など)は、もはや生の driver.Conn を直接保持せず、*driverConn のポインタを保持するようになりました。これにより、プールから接続が取得されるたびに、その接続に関連付けられたミューテックスが利用可能になります。

  3. ミューテックスによる保護の徹底: database/sql パッケージ内の、driver.Conn やその派生インターフェース(driver.Tx, driver.Stmt, driver.Rows, driver.Result)のメソッドを呼び出すほぼすべての箇所で、対応する driverConnLock()Unlock() が追加されました。 例えば、db.exec 関数では、driver.Execer インターフェースの Exec メソッドを呼び出す前に dc.Lock() を行い、呼び出し後に dc.Unlock() を行っています。

    // 旧:
    // if execer, ok := ci.(driver.Execer); ok {
    //     ...
    //     resi, err := execer.Exec(query, dargs)
    //     ...
    // }
    
    // 新:
    if execer, ok := dc.ci.(driver.Execer); ok {
        dargs, err := driverArgs(nil, args)
        if err != nil {
            return nil, err
        }
        dc.Lock() // ここでロック
        resi, err := execer.Exec(query, dargs)
        dc.Unlock() // ここでアンロック
        if err != driver.ErrSkip {
            if err != nil {
                return nil, err
            }
            return driverResult{dc, resi}, nil
        }
    }
    

    これにより、database/sql パッケージがドライバのインターフェースを呼び出す際には、常に排他的なアクセスが保証されます。

  4. withLock ヘルパー関数: withLock 関数は、ミューテックスのロックとアンロックを defer を使って安全に行うための簡潔な方法を提供します。これは、特に Close メソッドのように、エラーが発生しても確実にアンロックする必要がある場合に便利です。

    func withLock(lk sync.Locker, fn func()) {
        lk.Lock()
        fn()
        lk.Unlock()
    }
    

    例えば、sti.Close() の呼び出しが withLock でラップされています。

この変更は、database/sql パッケージがドライバとのインタラクションにおいて、より堅牢な並行性制御を内部的に行うための重要なステップです。これにより、ドライバの実装が driver.Conn の非同時使用の約束を厳密に守っているかどうかにかかわらず、database/sql パッケージが安全に動作することが保証されます。これは、将来的なバグの修正や機能追加の基盤となります。

関連リンク

参考にした情報源リンク