Commit fd378191 authored by cznic's avatar cznic
Browse files

Merge branch 'tx-shared-cache-wait' into 'master'

tx, conn: wait for a shared-cache lock in BeginTx, Commit and Rollback, and end such a wait with the context

Updates #192

See merge request !143

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parents 0fc3f90d 75b4e62f
Loading
Loading
Loading
Loading
+2
Changes for CHANGELOG.md: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,6 +7,8 @@ Entries for v1.38.1 through v1.44.1 and for v1.49.1 were added on 2026-09-05, re
     - Each release tag now gets a signed provenance document, linked from its GitLab release; `VERIFYING.md` says what it proves and how to check one. Tooling only. See [GitLab merge request #141](https://gitlab.com/cznic/sqlite/-/merge_requests/141).
     - Add an opt-in SQLite authorizer per connection: `AuthorizerRegisterer`, reached through `sql.Conn.Raw` or installed on every pooled connection from `RegisterConnectionHook`; existing connections are unchanged. Resolves [GitLab issue #256](https://gitlab.com/cznic/sqlite/-/issues/256). See [GitLab merge request #139](https://gitlab.com/cznic/sqlite/-/merge_requests/139), thanks Tedla Brandsema (@TedlaBrandsema)!
     - Closing a connection now forgets the pre-update, commit and rollback hooks registered on it; their entries used to linger after `Close`.
     - **With a shared cache, `BeginTx` now waits for a lock another connection holds** instead of failing at once with `SQLITE_LOCKED_SHAREDCACHE` (262), as executing `BEGIN IMMEDIATE` did; `Commit` and `Rollback` wait likewise while another connection changes the schema. `busy_timeout` does not bound the wait; the call's context does. See [GitLab merge request #143](https://gitlab.com/cznic/sqlite/-/merge_requests/143), thanks Ian Chechin!
     - `PrepareContext` and `BeginTx` with a context that is already done now return the context's error, as `ExecContext` and `QueryContext` do. See [GitLab merge request #143](https://gitlab.com/cznic/sqlite/-/merge_requests/143), thanks Ian Chechin!

 - 2026-09-29 v1.60.1:
     - Binding arguments to a statement is no longer quadratic in the number of its parameters, which made multi-row `INSERT`s with thousands of `?` parameters slow. Resolves [GitHub issue #8](https://github.com/modernc-org/sqlite/issues/8), thanks wencycool!
+74 −7
Changes for conn.go: 74 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ import (
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unsafe"

@@ -27,6 +28,16 @@ type conn struct {
	// concurrently.
	sync.Mutex

	// waitCtx is the context of the call in progress on this connection, if
	// it has one, so that a wait for a shared-cache lock in retry ends once
	// it is done. Calls on a connection do not overlap, but they nest, e.g.
	// preparing inside an exec, so each restores the outer call's.
	waitCtx context.Context

	// waitingForLock is set while retry waits for a shared-cache lock, so
	// that tests can see the wait.
	waitingForLock atomic.Bool

	writeTimeFormat   string
	beginMode         string
	loc               *time.Location
@@ -449,9 +460,16 @@ func (c *conn) step(pstmt uintptr) (int, error) {
	}
}

// retry waits until the connection holding the shared-cache lock that made
// pstmt, or the statement being prepared when pstmt is 0, return
// SQLITE_LOCKED_SHAREDCACHE releases it. busy_timeout does not apply to that
// code, so the wait is not bounded by it; it ends early, with the context's
// error, once the context of the call (waitCtx) is done, also if it is done
// before the wait begins.
func (c *conn) retry(pstmt uintptr) error {
	mu := mutexAlloc(c.tls)
	(*mutex)(unsafe.Pointer(mu)).Lock()
	m := (*mutex)(unsafe.Pointer(mu))
	m.Lock()
	rc := sqlite3.Xsqlite3_unlock_notify(
		c.tls,
		c.db,
@@ -461,14 +479,45 @@ func (c *conn) retry(pstmt uintptr) error {
		mu,
	)
	if rc == sqlite3.SQLITE_LOCKED { // Deadlock, see https://www.sqlite.org/c3ref/unlock_notify.html
		(*mutex)(unsafe.Pointer(mu)).Unlock()
		m.Unlock()
		mutexFree(c.tls, mu)
		return c.errstr(rc)
	}

	(*mutex)(unsafe.Pointer(mu)).Lock()
	(*mutex)(unsafe.Pointer(mu)).Unlock()
	// The callback or, once the context is done, ctxWake unlocks m, which
	// ends the wait. context.AfterFunc runs ctxWake at once if the context
	// is done already.
	ctx := c.waitCtx
	var ctxWoke bool
	var ctxWakeDone chan struct{}
	var stop func() bool
	if ctx != nil && ctx.Done() != nil {
		ctxWakeDone = make(chan struct{})
		stop = context.AfterFunc(ctx, func() {
			ctxWoke = m.wake()
			close(ctxWakeDone)
		})
	}

	c.waitingForLock.Store(true)
	m.Lock()
	c.waitingForLock.Store(false)

	// A ctxWake that has started may still use m, so it is waited for
	if stop != nil && !stop() {
		<-ctxWakeDone
	}

	// Cancels the callback if the context ended the wait, and in any case
	// returns only once no callback is running: SQLite invokes it, and
	// handles this call, under the same mutex, so mu can be freed
	sqlite3.Xsqlite3_unlock_notify(c.tls, c.db, 0, 0)
	mutexFree(c.tls, mu)

	if ctxWoke {
		return ctx.Err()
	}

	if pstmt != 0 {
		sqlite3.Xsqlite3_reset(c.tls, pstmt)
	}
@@ -1181,7 +1230,25 @@ func (c *conn) Prepare(query string) (ds driver.Stmt, err error) {
}

func (c *conn) prepare(ctx context.Context, query string) (s driver.Stmt, err error) {
	//TODO use ctx
	return c.newStmtContext(ctx, query)
}

// newStmtContext is newStmt for a call with a context: preparing a statement
// can wait for a shared-cache lock, and the wait ends early, with ctx.Err(),
// once ctx is done. Only the wait watches ctx, see retry.
func (c *conn) newStmtContext(ctx context.Context, query string) (*stmt, error) {
	if ctx != nil {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}
	}

	prevWaitCtx := c.waitCtx
	c.waitCtx = ctx
	defer func() { c.waitCtx = prevWaitCtx }()

	return newStmt(c, query)
}

@@ -1203,9 +1270,9 @@ func (c *conn) Query(query string, args []driver.Value) (dr driver.Rows, err err
}

func (c *conn) query(ctx context.Context, query string, args []driver.NamedValue) (r driver.Rows, err error) {
	// Use newStmt directly. c.prepare wraps this, but we need the concrete *stmt type
	// Use newStmtContext directly. c.prepare wraps this, but we need the concrete *stmt type
	// to manipulate the handle ownership below.
	s, err := newStmt(c, query)
	s, err := c.newStmtContext(ctx, query)
	if err != nil {
		return nil, err
	}
+16
Changes for mutex.go: 16 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -6,14 +6,30 @@ package sqlite // import "modernc.org/sqlite"

import (
	"sync"
	"sync/atomic"
	"unsafe"

	"modernc.org/libc"
	"modernc.org/libc/sys/types"
)

// mutex is what a connection waiting for a shared-cache lock blocks on, see
// (*conn).retry. It is unlocked once, by wake, either from the unlock-notify
// callback or because the context of the waiting call is done.
type mutex struct {
	sync.Mutex
	woken int32
}

// wake unlocks m unless it has already been woken, and reports whether this
// call did it.
func (m *mutex) wake() bool {
	if !atomic.CompareAndSwapInt32(&m.woken, 0, 1) {
		return false
	}

	m.Unlock()
	return true
}

func mutexAlloc(tls *libc.TLS) uintptr {

sharedcache_test.go

0 → 100644
+599
Changes for sharedcache_test.go: 599 added lines, 0 removed lines.
Original line number Diff line number Diff line
// Copyright 2026 The Sqlite Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sqlite

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"testing"
	"time"
	"unsafe"

	"modernc.org/libc"
)

// sharedCacheConns opens two connections to a fresh shared-cache in-memory
// database. When schemaLock is false, table t is created first, so that a
// write transaction on the first connection holds only a write lock;
// otherwise the first connection's transaction creates t and so also holds
// the schema.
func sharedCacheConns(t *testing.T, name string, schemaLock bool) (db *sql.DB, c1, c2 *sql.Conn, tx1 *sql.Tx) {
	t.Helper()

	ctx := context.Background()
	db, err := sql.Open(driverName, fmt.Sprintf("file:%s?mode=memory&cache=shared&_txlock=immediate", name))
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { db.Close() })
	db.SetMaxOpenConns(2)
	if c1, err = db.Conn(ctx); err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { c1.Close() })
	if c2, err = db.Conn(ctx); err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { c2.Close() })
	if !schemaLock {
		if _, err = c1.ExecContext(ctx, "CREATE TABLE t(x)"); err != nil {
			t.Fatal(err)
		}
	}

	if tx1, err = c1.BeginTx(ctx, nil); err != nil {
		t.Fatal(err)
	}

	// Releases the lock before the connections close, if the test has not
	t.Cleanup(func() { tx1.Rollback() })
	stmt := "INSERT INTO t VALUES(1)"
	if schemaLock {
		stmt = "CREATE TABLE t(x)"
	}
	if _, err = tx1.Exec(stmt); err != nil {
		t.Fatal(err)
	}

	return db, c1, c2, tx1
}

// driverConn returns the driver connection behind c.
func driverConn(t *testing.T, c *sql.Conn) (r *conn) {
	t.Helper()

	if err := c.Raw(func(dc any) error {
		r = dc.(*conn)
		return nil
	}); err != nil {
		t.Fatal(err)
	}

	return r
}

// awaitSharedCacheWait waits until dc is blocked waiting for a shared-cache
// lock, or until done delivers the result of the call that should have been
// waiting, which is then returned with returned set.
func awaitSharedCacheWait(t *testing.T, dc *conn, done <-chan error) (err error, returned bool) {
	t.Helper()

	deadline := time.Now().Add(10 * time.Second)
	for time.Now().Before(deadline) {
		select {
		case err := <-done:
			return err, true
		default:
		}

		if dc.waitingForLock.Load() {
			return nil, false
		}

		time.Sleep(time.Millisecond)
	}

	t.Fatal("the call neither returned nor waited for the shared-cache lock")
	return nil, false
}

// failWaitNotEnded reports a call that kept waiting after its context was
// done: it releases the lock with release and fails with what the call
// returned then, without waiting longer than ten seconds for it.
func failWaitNotEnded(t *testing.T, what string, release func() error, done <-chan error) {
	t.Helper()

	release()
	select {
	case err := <-done:
		t.Fatalf("%s did not return when its context was done; it returned %v once the lock was released", what, err)
	case <-time.After(10 * time.Second):
		t.Fatalf("%s did not return when its context was done, nor after the lock was released", what)
	}
}

// With a shared cache, a transaction begun while another connection holds
// the lock waits for it, as an executed BEGIN IMMEDIATE does, and starts
// once the lock is released, rather than failing at once with
// SQLITE_LOCKED_SHAREDCACHE. See https://gitlab.com/cznic/sqlite/-/issues/192.
func TestSharedCacheBeginTxWaits(t *testing.T) {
	for _, schemaLock := range []bool{false, true} {
		t.Run(fmt.Sprintf("schemaLock=%v", schemaLock), func(t *testing.T) {
			_, _, c2, tx1 := sharedCacheConns(t, fmt.Sprintf("sharedcache-begin-%v", schemaLock), schemaLock)
			dc2 := driverConn(t, c2)
			done := make(chan error, 1)
			var tx2 *sql.Tx
			go func() {
				var err error
				tx2, err = c2.BeginTx(context.Background(), nil)
				done <- err
			}()

			if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
				tx1.Rollback()
				t.Fatalf("BeginTx returned without waiting for the lock: %v", err)
			}

			if err := tx1.Commit(); err != nil {
				t.Fatal(err)
			}

			select {
			case err := <-done:
				if err != nil {
					t.Fatalf("BeginTx after the lock was released: %v", err)
				}
			case <-time.After(10 * time.Second):
				t.Fatal("BeginTx did not return after the lock was released")
			}

			if _, err := tx2.Exec("INSERT INTO t VALUES(2)"); err != nil {
				t.Fatal(err)
			}

			if err := tx2.Commit(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

// A call waiting for a shared-cache lock returns the context's error once
// the context is done, while the lock is still held, and leaves the
// connection usable.
func TestSharedCacheWaitEndsWithContext(t *testing.T) {
	for _, tc := range []struct {
		name       string
		schemaLock bool
		call       func(ctx context.Context, c *sql.Conn) error
	}{
		{"BeginTx", false, func(ctx context.Context, c *sql.Conn) error {
			tx, err := c.BeginTx(ctx, nil)
			if err == nil {
				tx.Rollback()
			}
			return err
		}},
		{"ExecContext", false, func(ctx context.Context, c *sql.Conn) error {
			_, err := c.ExecContext(ctx, "BEGIN IMMEDIATE")
			if err == nil {
				c.ExecContext(context.Background(), "ROLLBACK")
			}
			return err
		}},
		// Preparing a query that needs the schema another connection is
		// changing waits before the statement runs
		{"QueryContext", true, func(ctx context.Context, c *sql.Conn) error {
			rows, err := c.QueryContext(ctx, "SELECT x FROM t")
			if err == nil {
				rows.Close()
			}
			return err
		}},
		// Reading a table another connection is writing waits when the
		// statement runs
		{"QueryContextStep", false, func(ctx context.Context, c *sql.Conn) error {
			rows, err := c.QueryContext(ctx, "SELECT x FROM t")
			if err == nil {
				rows.Close()
			}
			return err
		}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			_, _, c2, tx1 := sharedCacheConns(t, "sharedcache-ctx-"+tc.name, tc.schemaLock)
			dc2 := driverConn(t, c2)
			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()
			done := make(chan error, 1)
			go func() { done <- tc.call(ctx, c2) }()

			if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
				tx1.Rollback()
				t.Fatalf("%s returned without waiting for the lock: %v", tc.name, err)
			}

			cancel()
			select {
			case err := <-done:
				if !errors.Is(err, context.Canceled) {
					t.Errorf("%s returned %v, want %v", tc.name, err, context.Canceled)
				}
			case <-time.After(10 * time.Second):
				failWaitNotEnded(t, tc.name, tx1.Rollback, done)
			}

			if err := tx1.Rollback(); err != nil {
				t.Fatal(err)
			}

			// The connection is usable again, with no wait or interrupt left over
			if _, err := c2.ExecContext(context.Background(), "BEGIN IMMEDIATE; ROLLBACK"); err != nil {
				t.Fatalf("the connection after the canceled wait: %v", err)
			}
		})
	}
}

// A context done before a wait for a shared-cache lock begins ends the wait
// at once with the context's error, canceled or past its deadline. Here the
// context is set on the connection directly, with no goroutine watching it,
// so nothing but retry can end the wait.
func TestSharedCacheWaitAfterContextDone(t *testing.T) {
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	expired, cancelExpired := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer cancelExpired()
	for _, tc := range []struct {
		name string
		ctx  context.Context
		want error
	}{
		{"Canceled", canceled, context.Canceled},
		{"DeadlineExceeded", expired, context.DeadlineExceeded},
	} {
		t.Run(tc.name, func(t *testing.T) {
			_, _, c2, tx1 := sharedCacheConns(t, "sharedcache-early-"+tc.name, false)
			dc2 := driverConn(t, c2)
			s, err := newStmt(dc2, "BEGIN IMMEDIATE")
			if err != nil {
				t.Fatal(err)
			}

			defer s.Close()
			dc2.waitCtx = tc.ctx
			defer func() { dc2.waitCtx = nil }()

			done := make(chan error, 1)
			go func() {
				_, err := dc2.step(s.pstmt)
				done <- err
			}()

			select {
			case err := <-done:
				if !errors.Is(err, tc.want) {
					t.Errorf("the wait ended with %v, want %v", err, tc.want)
				}
			case <-time.After(10 * time.Second):
				failWaitNotEnded(t, "the wait", tx1.Rollback, done)
			}
		})
	}
}

// interruptOnDone and newStmtContext set the context that ends a wait for
// the call they serve and restore the outer call's afterwards, so that a
// later wait is not ended by a context that belonged to a finished call.
func TestSharedCacheWaitContextRestored(t *testing.T) {
	_, _, c2, tx1 := sharedCacheConns(t, "sharedcache-restored", false)
	dc2 := driverConn(t, c2)

	outer := context.WithValue(context.Background(), struct{}{}, "outer")
	inner, innerCancel := context.WithCancel(context.Background())
	dc2.waitCtx = outer
	cleanup := interruptOnDone(inner, dc2, nil)
	if dc2.waitCtx != inner {
		t.Error("interruptOnDone did not record the context of the call")
	}
	cleanup()
	innerCancel()
	if dc2.waitCtx != outer {
		t.Error("interruptOnDone did not restore the outer call's context")
	}
	dc2.waitCtx = nil

	// A statement prepared with a context that is canceled afterwards and
	// then executed without one, so that nothing sets the context of the
	// execution: its wait must not be ended by the preparation's context
	ctx, cancel := context.WithCancel(context.Background())
	stmt, err := c2.PrepareContext(ctx, "INSERT INTO t VALUES(3)")
	if err != nil {
		t.Fatal(err)
	}

	defer stmt.Close()
	cancel()
	done := make(chan error, 1)
	go func() {
		_, err := stmt.ExecContext(context.Background())
		done <- err
	}()

	if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
		t.Fatalf("the statement returned without waiting for the lock: %v", err)
	}

	if err := tx1.Commit(); err != nil {
		t.Fatal(err)
	}

	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("the statement after the lock was released: %v", err)
		}
	case <-time.After(10 * time.Second):
		t.Fatal("the statement did not return after the lock was released")
	}
}

// A wait that the lock's release ends, under a context canceled only
// afterwards, leaves nothing behind that ends or disturbs a later wait.
func TestSharedCacheWaitThenLateCancel(t *testing.T) {
	_, c1, c2, tx1 := sharedCacheConns(t, "sharedcache-late-cancel", false)
	dc2 := driverConn(t, c2)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	done := make(chan error, 1)
	go func() {
		_, err := c2.ExecContext(ctx, "BEGIN IMMEDIATE")
		done <- err
	}()

	if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
		t.Fatalf("BEGIN IMMEDIATE returned without waiting for the lock: %v", err)
	}

	if err := tx1.Commit(); err != nil {
		t.Fatal(err)
	}

	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("BEGIN IMMEDIATE after the lock was released: %v", err)
		}
	case <-time.After(10 * time.Second):
		t.Fatal("BEGIN IMMEDIATE did not return after the lock was released")
	}

	cancel()
	if _, err := c2.ExecContext(context.Background(), "ROLLBACK"); err != nil {
		t.Fatal(err)
	}

	tx1b, err := c1.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { tx1b.Rollback() })
	if _, err := tx1b.Exec("INSERT INTO t VALUES(2)"); err != nil {
		t.Fatal(err)
	}

	go func() {
		tx2, err := c2.BeginTx(context.Background(), nil)
		if err == nil {
			err = tx2.Rollback()
		}
		done <- err
	}()

	if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
		t.Fatalf("the second BeginTx returned without waiting for the lock: %v", err)
	}

	if err := tx1b.Commit(); err != nil {
		t.Fatal(err)
	}

	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("the second BeginTx: %v", err)
		}
	case <-time.After(10 * time.Second):
		t.Fatal("the second BeginTx did not return after the lock was released")
	}
}

// Preparing COMMIT or ROLLBACK, like any statement, needs the schema, so
// while another connection is changing it, ending even an empty transaction
// waits for the change, where it failed at once with
// SQLITE_LOCKED_SHAREDCACHE.
func TestSharedCacheTxEndWaitsForSchemaLock(t *testing.T) {
	for _, commit := range []bool{true, false} {
		t.Run(fmt.Sprintf("commit=%v", commit), func(t *testing.T) {
			ctx := context.Background()
			db, err := sql.Open(driverName, fmt.Sprintf("file:sharedcache-txend-%v?mode=memory&cache=shared", commit))
			if err != nil {
				t.Fatal(err)
			}

			t.Cleanup(func() { db.Close() })
			db.SetMaxOpenConns(2)
			c1, err := db.Conn(ctx)
			if err != nil {
				t.Fatal(err)
			}

			t.Cleanup(func() { c1.Close() })
			c2, err := db.Conn(ctx)
			if err != nil {
				t.Fatal(err)
			}

			t.Cleanup(func() { c2.Close() })
			dc2 := driverConn(t, c2)
			tx2, err := c2.BeginTx(ctx, nil)
			if err != nil {
				t.Fatal(err)
			}

			tx1, err := c1.BeginTx(ctx, nil)
			if err != nil {
				t.Fatal(err)
			}

			t.Cleanup(func() { tx1.Rollback() })
			if _, err := tx1.Exec("CREATE TABLE t(x)"); err != nil {
				t.Fatal(err)
			}

			done := make(chan error, 1)
			go func() {
				if commit {
					done <- tx2.Commit()
				} else {
					done <- tx2.Rollback()
				}
			}()

			if err, returned := awaitSharedCacheWait(t, dc2, done); returned {
				t.Fatalf("the transaction ended without waiting for the schema lock: %v", err)
			}

			if err := tx1.Commit(); err != nil {
				t.Fatal(err)
			}

			select {
			case err := <-done:
				if err != nil {
					t.Fatalf("ending the transaction after the schema lock was released: %v", err)
				}
			case <-time.After(10 * time.Second):
				t.Fatal("the transaction did not end after the schema lock was released")
			}
		})
	}
}

// A context done after the call starts but before it waits for a shared-cache
// lock, here by an authorizer that cancels it while BEGIN is prepared, ends
// the wait at once with the context's error.
func TestSharedCacheBeginTxContextDoneBeforeWait(t *testing.T) {
	_, _, c2, tx1 := sharedCacheConns(t, "sharedcache-done-before-wait", false)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	setAuthorizer := func(fn AuthorizerFn) {
		t.Helper()
		if err := c2.Raw(func(dc any) error {
			return dc.(AuthorizerRegisterer).RegisterAuthorizer(fn)
		}); err != nil {
			t.Fatal(err)
		}
	}

	setAuthorizer(func(action AuthorizerActionCode, _, _, _, _ string) AuthorizerReturnCode {
		if action == AuthTransaction {
			cancel()
		}
		return AuthorizerOK
	})
	done := make(chan error, 1)
	go func() {
		tx, err := c2.BeginTx(ctx, nil)
		if err == nil {
			tx.Rollback()
		}
		done <- err
	}()

	select {
	case err := <-done:
		if !errors.Is(err, context.Canceled) {
			t.Errorf("BeginTx returned %v, want %v", err, context.Canceled)
		}
	case <-time.After(10 * time.Second):
		failWaitNotEnded(t, "BeginTx", tx1.Rollback, done)
	}

	if err := tx1.Rollback(); err != nil {
		t.Fatal(err)
	}

	setAuthorizer(nil)
	if _, err := c2.ExecContext(context.Background(), "BEGIN IMMEDIATE; ROLLBACK"); err != nil {
		t.Fatalf("the connection after the canceled wait: %v", err)
	}
}

// Preparing a statement or beginning a transaction with a context that is
// already done returns the context's error, as executing and querying do,
// rather than preparing, or beginning a transaction that database/sql then
// rolls back.
func TestPrepareAndBeginTxContextDone(t *testing.T) {
	db, err := sql.Open(driverName, ":memory:")
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { db.Close() })
	c, err := db.Conn(context.Background())
	if err != nil {
		t.Fatal(err)
	}

	t.Cleanup(func() { c.Close() })
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if s, err := c.PrepareContext(ctx, "SELECT 1"); !errors.Is(err, context.Canceled) {
		if s != nil {
			s.Close()
		}
		t.Errorf("PrepareContext returned %v, want %v", err, context.Canceled)
	}

	if tx, err := c.BeginTx(ctx, nil); !errors.Is(err, context.Canceled) {
		if tx != nil {
			tx.Rollback()
		}
		t.Errorf("BeginTx returned %v, want %v", err, context.Canceled)
	}
}

// The unlock-notify callback and the context can both try to end a wait, so
// wake unlocks the mutex once, for whichever comes first, and reports which
// one did; a second unlock would be a fatal error.
func TestMutexWakeOnce(t *testing.T) {
	tls := libc.NewTLS()
	defer tls.Close()

	for i := 0; i < 1000; i++ {
		mu := mutexAlloc(tls)
		m := (*mutex)(unsafe.Pointer(mu))
		m.Lock()
		woke := make(chan bool, 2)
		for j := 0; j < 2; j++ {
			go func() { woke <- m.wake() }()
		}

		m.Lock()
		if a, b := <-woke, <-woke; a == b {
			t.Fatalf("wake reported %v and %v, want exactly one true", a, b)
		}

		m.Unlock()
		mutexFree(tls, mu)
	}
}
+7 −1
Changes for sqlite.go: 7 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -89,6 +89,11 @@ func interruptOnDone(

	donech := make(chan struct{})

	// A wait for a shared-cache lock during the call ends once ctx is done,
	// see retry; the outer call's context is restored afterwards
	prevWaitCtx := c.waitCtx
	c.waitCtx = ctx

	go func() {
		select {
		case <-ctx.Done():
@@ -112,6 +117,7 @@ func interruptOnDone(
		atomic.StoreInt32(done, 1)
		donemu.Unlock()
		close(donech)
		c.waitCtx = prevWaitCtx
	}
}

@@ -497,7 +503,7 @@ func applyQueryParams(c *conn, query string, defensive bool) error {
func unlockNotify(t *libc.TLS, ppArg uintptr, nArg int32) {
	for i := int32(0); i < nArg; i++ {
		mu := *(*uintptr)(unsafe.Pointer(ppArg))
		(*mutex)(unsafe.Pointer(mu)).Unlock()
		(*mutex)(unsafe.Pointer(mu)).wake()
		ppArg += ptrSize
	}
}
Loading