diff --git a/.github/workflows/test-sqlite.yml b/.github/workflows/test-sqlite.yml index 1c3c0c9..6ea6f03 100644 --- a/.github/workflows/test-sqlite.yml +++ b/.github/workflows/test-sqlite.yml @@ -28,7 +28,9 @@ jobs: - name: With sqlite_enable_tmstmpvfs run: go test -v -tags sqlite_enable_tmstmpvfs - + + - name: With sqlite_enable_multithreaded_checks + run: go test -v -tags sqlite_enable_multithreaded_checks - name: Race run: go test -v -race ./... diff --git a/cgosqlite/cgosqlite.go b/cgosqlite/cgosqlite.go index 7a5f617..dd61314 100644 --- a/cgosqlite/cgosqlite.go +++ b/cgosqlite/cgosqlite.go @@ -63,6 +63,14 @@ int tmstmpvfs_enabled=1; int tmstmpvfs_enabled=0; #endif +// Enable mutex contention warnings. +#cgo sqlite_enable_multithreaded_checks CFLAGS: -DSQLITE_ENABLE_MULTITHREADED_CHECKS +#ifdef SQLITE_ENABLE_MULTITHREADED_CHECKS +int multithreaded_checks_enabled=1; +#else +int multithreaded_checks_enabled=0; +#endif + #include "cgosqlite.h" */ import "C" @@ -529,3 +537,8 @@ func APIArmorEnabled() bool { func TimestampVFSEnabled() bool { return C.tmstmpvfs_enabled == 1 } + +// MultithreadedChecksEnabled reports whether or not sqlite was compiled with SQLITE_ENABLE_MULTITHREADED_CHECKS. +func MultithreadedChecksEnabled() bool { + return C.multithreaded_checks_enabled == 1 +} diff --git a/cgosqlite/multithreaded_checks_disabled_test.go b/cgosqlite/multithreaded_checks_disabled_test.go new file mode 100644 index 0000000..98ae540 --- /dev/null +++ b/cgosqlite/multithreaded_checks_disabled_test.go @@ -0,0 +1,11 @@ +//go:build !sqlite_enable_multithreaded_checks + +package cgosqlite + +import ( + "testing" +) + +func TestMultithreadedChecksDisabled(t *testing.T) { + testMultithreadedChecks(t, false) +} diff --git a/cgosqlite/multithreaded_checks_enabled_test.go b/cgosqlite/multithreaded_checks_enabled_test.go new file mode 100644 index 0000000..cf18c31 --- /dev/null +++ b/cgosqlite/multithreaded_checks_enabled_test.go @@ -0,0 +1,11 @@ +//go:build sqlite_enable_multithreaded_checks + +package cgosqlite + +import ( + "testing" +) + +func TestMultithreadedChecksEnabled(t *testing.T) { + testMultithreadedChecks(t, true) +} diff --git a/cgosqlite/multithreaded_checks_test.go b/cgosqlite/multithreaded_checks_test.go new file mode 100644 index 0000000..bfc305b --- /dev/null +++ b/cgosqlite/multithreaded_checks_test.go @@ -0,0 +1,80 @@ +// + +package cgosqlite + +import ( + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "testing" + + "github.com/tailscale/sqlite/sqliteh" +) + +// testMultithreadedChecks provides a common function for testing SQLITE_ENABLE_MULTITHREADED_CHECKS. +func testMultithreadedChecks(t *testing.T, wantThreadingWarning bool) { + if wantThreadingWarning && !MultithreadedChecksEnabled() { + t.Fatal("Multithreaded checks are not enabled") + } else if !wantThreadingWarning && MultithreadedChecksEnabled() { + t.Fatal("Multithreaded checks are enabled") + } + + var gotMisuseLog atomic.Bool + err := SetLogCallback(func(code sqliteh.Code, msg string) { + if code == sqliteh.SQLITE_MISUSE && msg == "illegal multi-threaded access to database connection" { + gotMisuseLog.Store(true) + } + }) + if err != nil { + t.Fatal(err) + } + + // Lock this goroutine to a thread (preventing other goroutines from using that thread) + runtime.LockOSThread() + + flags := sqliteh.SQLITE_OPEN_READWRITE | + sqliteh.SQLITE_OPEN_CREATE | + sqliteh.SQLITE_OPEN_WAL | + sqliteh.SQLITE_OPEN_URI | + sqliteh.SQLITE_OPEN_FULLMUTEX + // sqliteh.SQLITE_OPEN_FULLMUTEX currently contention checks do not work when opening connection with SQLITE_OPEN_FULLMUTEX + db, err := Open(filepath.Join(t.TempDir(), "test.db"), flags, "") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + hitAPI := func() { + for i := 0; i < 1000 && !gotMisuseLog.Load(); i++ { + // Prepare a statement on this thread, mostly ignoring errors. + stmt, _, err := db.Prepare("CREATE TABLE t(c INTEGER PRIMARY KEY)", 0) + if err != nil { + continue + } + if _, err := stmt.Step(nil); err != nil { + continue + } + _ = stmt.Finalize() + } + } + + // Hit API on a separate goroutine as well as in this goroutine. + // Because the original goroutine locked the OS thread, this new goroutine + // will execute on a separate thread. + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + hitAPI() + }() + hitAPI() + wg.Wait() + + if wantThreadingWarning && !gotMisuseLog.Load() { + t.Fatal("did not get SQLITE_MISUSE in LogCallback") + } + if !wantThreadingWarning && gotMisuseLog.Load() { + t.Fatal("got SQLITE_MISUSE in LogCallback") + } +} diff --git a/cgosqlite/sqlite3.c b/cgosqlite/sqlite3.c index b44fb75..4df33f9 100644 --- a/cgosqlite/sqlite3.c +++ b/cgosqlite/sqlite3.c @@ -188262,7 +188262,7 @@ static int openDatabase( db = 0; goto opendb_out; } - if( isThreadsafe==0 ){ + if( isThreadsafe==0 || sqlite3GlobalConfig.bCoreMutex ){ sqlite3MutexWarnOnContention(db->mutex); } }