Skip to content

🎯 Prevent Connection Leak to Pool on Context Cancellation during BeginTx() #2

Description

@raimeecas

📝 Description

When initiating a transaction using BeginTx(ctx, opts) in the raimeecas/mysql driver, a context cancellation or timeout occurring during the transaction setup phase can lead to a connection state mismatch. Specifically, if the driver successfully sends the START TRANSACTION (or BEGIN) command to the MySQL server, but the context is canceled before the transaction is fully established and returned to the user, the connection may be returned to the connection pool while still in an active transaction state.

This results in "dirty" connections in the pool. Subsequent queries executing on these connections will unexpectedly run inside the uncommitted transaction, leading to potential data isolation issues, lock contention, and silent failures.

🎯 Acceptance Criteria

  • If the context passed to BeginTx is canceled or times out during the transaction initialization, the driver must guarantee that the connection is not returned to the pool in an active transaction state.
  • If a context cancellation occurs during BeginTx, the driver must attempt to roll back the transaction (ROLLBACK) or, if the connection state is uncertain, discard/close the connection entirely so it is not reused.
  • The connection pool must not leak active transactions under high concurrency and frequent context timeouts.
  • The implementation must not introduce race conditions between the query execution goroutine and the context cancellation listener.

🛠️ Technical Specifications & Context

In Go's database/sql driver interface, Conn.BeginTx(ctx context.Context, opts driver.TxOptions) is responsible for starting a transaction.

Potential Areas of Modification:

  • connection.go / driver.go (Transaction Initialization):
    Within the implementation of BeginTx, look for where the START TRANSACTION query is written to the network socket.
    If ctx.Done() is triggered during or immediately after sending the query but before returning the driver.Tx implementation, the connection state is dirty.

  • Connection Discard Policy:
    If the context is canceled, executing a ROLLBACK over the same connection might fail if the context is already expired. In such cases, the driver should return driver.ErrBadConn to signal to the database/sql connection pool that the connection is compromised and must be closed and discarded rather than returned to the free pool.

Example pattern to implement in BeginTx:

// Inside BeginTx implementation
if err := ctx.Err(); err != nil {
    return nil, err
}

// Send BEGIN command to MySQL
err := mc.writeCommandPacketStr(comQuery, "START TRANSACTION")
if err != nil {
    return nil, err
}

// Check if context was canceled during the roundtrip
select {
case <-ctx.Done():
    // The transaction started on the server, but the context is dead.
    // We must close the connection to prevent returning a dirty connection to the pool.
    mc.Close() 
    return nil, ctx.Err()
default:
    // Proceed normally
}

🧪 Verification & Testing

To verify the fix, implement a test case that simulates a context cancellation during transaction start:

  1. Unit/Integration Test:
    Create a test that calls BeginTx with a pre-canceled context or a context that cancels concurrently during the connection setup.
    ctx, cancel := context.WithCancel(context.Background())
    // Trigger cancellation immediately or with a tiny delay to race with BeginTx
    cancel() 
    
    tx, err := db.BeginTx(ctx, nil)
    if err == nil {
        tx.Rollback()
        t.Fatal("Expected BeginTx to fail with context canceled")
    }
  2. Pool Cleanliness Verification:
    After the failed BeginTx, acquire a new connection from the pool and verify it is not in a transaction:
    var inTransaction int
    // In MySQL, check if we are in a transaction (e.g., by checking @@in_transaction or performing a test write/rollback)
    err = db.QueryRow("SELECT @@in_transaction").Scan(&inTransaction)
    if err != nil {
        t.Fatal(err)
    }
    if inTransaction != 0 {
        t.Error("Connection returned to the pool was left in an active transaction state!")
    }

Opire Bounty


This repo is using Opire - what does it mean? 👇
💵 Everyone can add rewards for this issue commenting /reward 100 (replace 100 with the amount).
🕵️‍♂️ If someone starts working on this issue to earn the rewards, they can comment /try to let everyone know!
🙌 And when they open the PR, they can comment /claim #2 either in the PR description or in a PR's comment.

🪙 Also, everyone can tip any user commenting /tip 20 @raimeecas (replace 20 with the amount, and @raimeecas with the user to tip).

📖 If you want to learn more, check out our documentation.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions