From 3b9010d673d525e10317e678ba5a09eae6ce9aef Mon Sep 17 00:00:00 2001 From: SirLouen Date: Wed, 21 Oct 2026 23:35:03 +0200 Subject: [PATCH 1/3] fix(dbkit/sqlite): run on modernc.org/sqlite v1.61.0 and libc v1.79.0 --- dbkit/sqlite/CHANGELOG.md | 2 ++ dbkit/sqlite/busy.go | 4 +-- dbkit/sqlite/busy_internal_test.go | 6 ++-- dbkit/sqlite/busy_test.go | 31 +++++++++++++++++-- dbkit/sqlite/classify.go | 2 +- dbkit/sqlite/go.mod | 12 ++++---- dbkit/sqlite/go.sum | 48 +++++++++++++++--------------- 7 files changed, 67 insertions(+), 38 deletions(-) diff --git a/dbkit/sqlite/CHANGELOG.md b/dbkit/sqlite/CHANGELOG.md index 829d361..8096701 100644 --- a/dbkit/sqlite/CHANGELOG.md +++ b/dbkit/sqlite/CHANGELOG.md @@ -12,6 +12,8 @@ Releases of this module are tagged `dbkit/sqlite/vX.Y.Z`. ### Changed +- **Breaking.** Requires `modernc.org/sqlite` v1.61.0 and `modernc.org/libc` v1.79.0, and `LibcVersion` is v1.79.0. +- **Breaking.** A `BeginTx` error after its context ends matches only the context error, no longer `dbkit.ErrBusy`, unless opening its connection answered busy. - The module needs Go 1.27.3. ## [0.2.0] - 2026-10-20 diff --git a/dbkit/sqlite/busy.go b/dbkit/sqlite/busy.go index 47b53bc..1b14ad6 100644 --- a/dbkit/sqlite/busy.go +++ b/dbkit/sqlite/busy.go @@ -162,12 +162,12 @@ func (w *busyWait) during(ctx context.Context) func() { return func() { w.span.Store(nil) } } -// BeginTx starts a transaction, and an error after ctx ended also matches the error of ctx. +// BeginTx starts a transaction, and answers the error of ctx alone when it failed after ctx ended. func (c *busyConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { defer c.wait.during(ctx)() tx, err := c.driverConn.BeginTx(ctx, opts) if err != nil { - return nil, ended(ctx, err) + return nil, interrupted(ctx, err) } return tx, nil } diff --git a/dbkit/sqlite/busy_internal_test.go b/dbkit/sqlite/busy_internal_test.go index 3fdde3c..eb690ff 100644 --- a/dbkit/sqlite/busy_internal_test.go +++ b/dbkit/sqlite/busy_internal_test.go @@ -360,7 +360,7 @@ func TestAStatementAfterItsContextEndedAnswersTheContextError(t *testing.T) { } } -func TestABeginAfterItsContextEndedAlsoAnswersTheContextError(t *testing.T) { +func TestABeginAfterItsContextEndedAnswersTheContextError(t *testing.T) { t.Parallel() busy := internalBusyError(t) @@ -371,8 +371,8 @@ func TestABeginAfterItsContextEndedAlsoAnswersTheContextError(t *testing.T) { _, err := conn.BeginTx(cancelled, driver.TxOptions{}) _, live := conn.BeginTx(t.Context(), driver.TxOptions{}) - if !errors.Is(err, busy) || !errors.Is(err, context.Canceled) { - t.Errorf("BeginTx() under a cancelled context error = %v, want one matching both busy and context.Canceled", err) + if err != context.Canceled { + t.Errorf("BeginTx() under a cancelled context error = %v, want exactly context.Canceled", err) } if live != busy { t.Errorf("BeginTx() under a live context error = %v, want the busy error as it is", live) diff --git a/dbkit/sqlite/busy_test.go b/dbkit/sqlite/busy_test.go index 71a91e2..1343e19 100644 --- a/dbkit/sqlite/busy_test.go +++ b/dbkit/sqlite/busy_test.go @@ -7,6 +7,7 @@ import ( "database/sql" "database/sql/driver" "errors" + "fmt" "math" "path/filepath" "slices" @@ -56,6 +57,8 @@ const ( busyTypedRead = "SELECT amount, label FROM typed_rows" // busyNoTable is a query that reads no table, so its open rows hold no read lock. busyNoTable = "VALUES (1), (2)" + // busySetTimeout sets a connection's own busy timeout to its one argument in milliseconds. + busySetTimeout = "PRAGMA busy_timeout=%d" ) // busyShape is one call that waits for the write lock. @@ -86,13 +89,13 @@ var busyShapes = []busyShape{ _ = tx.Rollback() } return err - }, true, true}, + }, false, true}, {"InTx", func(ctx context.Context, db *sql.DB, _ *sql.Stmt) error { return dbkit.InTx(ctx, db, func(tx *sql.Tx) error { _, err := tx.ExecContext(ctx, busyInsert) return err }) - }, true, false}, + }, false, false}, {"a statement prepared before the writer", func(ctx context.Context, _ *sql.DB, prepared *sql.Stmt) error { _, err := prepared.ExecContext(ctx) return err @@ -227,6 +230,30 @@ func TestABusyWaitEndsWhenCancelled(t *testing.T) { busyMustEndEvery(t, busyCancelOf, context.Canceled) } +func TestACallersOwnBusyTimeoutStillEndsAtItsDeadline(t *testing.T) { + t.Parallel() + + db := rulesOpen(t, testOptions()) + rulesExec(t, db, busySchema) + pinned := busyPin(t, db) + busyPinExec(t, pinned, fmt.Sprintf(busySetTimeout, optionsBusyTimeout.Milliseconds())) + rulesHoldWriter(t, db, busyOtherInsert) + ctx, cancel := busyDeadlineOf(t.Context()) + defer cancel() + began := time.Now() + + _, err := pinned.ExecContext(ctx, busyInsert) + + waited := time.Since(began) + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("the insert error = %v, want one matching context.DeadlineExceeded", err) + } + if waited > busyBound { + t.Errorf("the insert answered after %v, want within a tenth of the busy timeout %v it set itself", waited, + optionsBusyTimeout) + } +} + // busyPin returns a connection of db pinned for the rest of the test. func busyPin(t *testing.T, db *sql.DB) *sql.Conn { t.Helper() diff --git a/dbkit/sqlite/classify.go b/dbkit/sqlite/classify.go index 3f1d63b..656ec27 100644 --- a/dbkit/sqlite/classify.go +++ b/dbkit/sqlite/classify.go @@ -14,7 +14,7 @@ import ( ) // LibcVersion is the modernc.org/libc version the driver is pinned with. -const LibcVersion = "v1.77.1" +const LibcVersion = "v1.79.0" // restrictMessage is the text SQLite gives a foreign key a RESTRICT action stops. const restrictMessage = "FOREIGN KEY constraint failed" diff --git a/dbkit/sqlite/go.mod b/dbkit/sqlite/go.mod index 09eaae8..de23f14 100644 --- a/dbkit/sqlite/go.mod +++ b/dbkit/sqlite/go.mod @@ -5,21 +5,21 @@ go 1.27.3 require ( github.com/gopherium/framework/dbkit v0.3.0 github.com/pressly/goose/v3 v3.27.2 - golang.org/x/sys v0.48.0 - modernc.org/libc v1.77.1 - modernc.org/sqlite v1.60.1 + golang.org/x/sys v0.49.0 + modernc.org/libc v1.79.0 + modernc.org/sqlite v1.61.0 ) require ( - github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dustin/go-humanize v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/ncruces/go-strftime v1.1.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sethvargo/go-retry v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.23.0 // indirect + golang.org/x/sync v0.24.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect ) diff --git a/dbkit/sqlite/go.sum b/dbkit/sqlite/go.sum index 205f60d..389cce3 100644 --- a/dbkit/sqlite/go.sum +++ b/dbkit/sqlite/go.sum @@ -1,9 +1,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= -github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= +github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= +github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs= +github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherium/framework/dbkit v0.3.0 h1:EkMU4dg6YkiIrn3SJH0oJ2KzLNIXrDMu4K+HHoraEIs= @@ -14,8 +14,8 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk= +github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.27.2 h1:FjKNzcmMdGrQlSIu5alMSmakQtJFBgtw+A0bb1p/LC8= @@ -28,30 +28,30 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= -modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= -modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA= +modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys= +modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4= +modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= -modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o= +modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs= -modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0= +modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8= +modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= @@ -60,8 +60,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw= -modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0= +modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY= +modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From bcacef0ce70e0de8fa6ee4d029646a98b0ba2c3d Mon Sep 17 00:00:00 2001 From: SirLouen Date: Thu, 22 Oct 2026 02:43:28 +0200 Subject: [PATCH 2/3] refactor(dbkit/sqlite): wait for a lock through the driver's busy handler --- dbkit/sqlite/.golangci.yml | 1 - dbkit/sqlite/CHANGELOG.md | 8 + dbkit/sqlite/busy.go | 331 ---------- dbkit/sqlite/busy_calls_internal_test.go | 281 -------- dbkit/sqlite/busy_internal_test.go | 775 ----------------------- dbkit/sqlite/busy_test.go | 173 ++--- dbkit/sqlite/connector.go | 35 +- dbkit/sqlite/connector_internal_test.go | 102 ++- dbkit/sqlite/connector_test.go | 32 +- dbkit/sqlite/go.mod | 2 +- dbkit/sqlite/options.go | 2 +- dbkit/sqlite/rules_test.go | 7 +- dbkit/sqlite/share_test.go | 2 +- dbkit/sqlite/sqlitetest/faults_test.go | 4 +- dbkit/sqlite/sqlitetest/template_test.go | 2 +- 15 files changed, 191 insertions(+), 1566 deletions(-) delete mode 100644 dbkit/sqlite/busy.go delete mode 100644 dbkit/sqlite/busy_calls_internal_test.go delete mode 100644 dbkit/sqlite/busy_internal_test.go diff --git a/dbkit/sqlite/.golangci.yml b/dbkit/sqlite/.golangci.yml index ac4c92e..be7782a 100644 --- a/dbkit/sqlite/.golangci.yml +++ b/dbkit/sqlite/.golangci.yml @@ -32,7 +32,6 @@ linters: - github.com/gopherium/framework/dbkit - github.com/pressly/goose/v3 - modernc.org/sqlite - - modernc.org/libc - golang.org/x/sys exclusions: rules: diff --git a/dbkit/sqlite/CHANGELOG.md b/dbkit/sqlite/CHANGELOG.md index 8096701..7ff749b 100644 --- a/dbkit/sqlite/CHANGELOG.md +++ b/dbkit/sqlite/CHANGELOG.md @@ -14,8 +14,16 @@ Releases of this module are tagged `dbkit/sqlite/vX.Y.Z`. - **Breaking.** Requires `modernc.org/sqlite` v1.61.0 and `modernc.org/libc` v1.79.0, and `LibcVersion` is v1.79.0. - **Breaking.** A `BeginTx` error after its context ends matches only the context error, no longer `dbkit.ErrBusy`, unless opening its connection answered busy. +- **Breaking.** A lock wait that starts while reading a later row of a query, or closing its rows, ends at `BusyTimeout` and not with the query's context. +- **Breaking.** A connection waiting for a lock tries again on SQLite's own schedule, up to every 100ms, so `BusyPoll` only paces opening a connection. +- Each connection waits for a lock through the driver's own busy handler, so `PRAGMA busy_timeout` reads `BusyTimeout` again. +- `sql.Conn.Raw` hands out the driver's own connection again. - The module needs Go 1.27.3. +### Removed + +- dbkit's own busy handler and its read of a private field of the driver. + ## [0.2.0] - 2026-10-20 ### Added diff --git a/dbkit/sqlite/busy.go b/dbkit/sqlite/busy.go deleted file mode 100644 index 1b14ad6..0000000 --- a/dbkit/sqlite/busy.go +++ /dev/null @@ -1,331 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package sqlite - -import ( - "context" - "database/sql/driver" - "fmt" - "io" - "reflect" - "sync" - "sync/atomic" - "time" - "unsafe" - - "modernc.org/libc" - sqlite3 "modernc.org/sqlite/lib" -) - -// busyWait is how one connection waits for a lock. -type busyWait struct { - // db is the connection's SQLite handle. - db uintptr - // timeout is BusyTimeout. - timeout time.Duration - // poll is BusyPoll. - poll time.Duration - // began is when the current wait outside a call began. - began time.Time - // span is the driver call running on the connection, nil between calls. - span atomic.Pointer[callSpan] -} - -// callSpan is one driver call on a connection. -type callSpan struct { - // done is the Done channel of the call's context, nil for a context that never ends. - done <-chan struct{} - // began is when the call's current wait began, zero until the call first waits. - began time.Time -} - -var ( - // busyWaits maps the id SQLite hands busyHandler to the wait of its connection. - busyWaits sync.Map - // lastWaitID is the id of the newest wait. - lastWaitID atomic.Uintptr -) - -// busyHandler is SQLite's busy callback for a dbkit connection, 1 to try the lock again and 0 to answer busy. -func busyHandler(tls *libc.TLS, id uintptr, count int32) int32 { - v, ok := busyWaits.Load(id) - if !ok { - return 0 - } - w := v.(*busyWait) - if sqlite3.Xsqlite3_is_interrupted(tls, w.db) != 0 || !w.retry(count) { - return 0 - } - return 1 -} - -// retry waits one busy poll at most and reports whether to try the lock again. -func (w *busyWait) retry(count int32) bool { - began, done := w.current() - if count == 0 || began.IsZero() { - *began = time.Now() - } - left := w.timeout - time.Since(*began) - if left <= 0 { - return false - } - poll := time.NewTimer(min(w.poll, left)) - defer poll.Stop() - select { - case <-done: - return false - case <-poll.C: - return true - } -} - -// current returns the start of the running wait and the Done channel of the running call, nil outside a call. -func (w *busyWait) current() (*time.Time, <-chan struct{}) { - if s := w.span.Load(); s != nil { - return &s.began, s.done - } - return &w.began, nil -} - -// busyHandlerFP is busyHandler as the function pointer SQLite calls. -var busyHandlerFP = *(*uintptr)(unsafe.Pointer(&struct { - f func(*libc.TLS, uintptr, int32) int32 -}{busyHandler})) - -// handleOf returns the SQLite handle of a connection the driver opened, read from the driver's private field db. -func handleOf(conn driver.Conn) (uintptr, error) { - v := reflect.ValueOf(conn) - if v.Kind() == reflect.Pointer { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return 0, fmt.Errorf("dbkit: the SQLite driver connection %T is not a struct, so a busy wait cannot end "+ - "with its context", conn) - } - field, ok := v.Type().FieldByName("db") - if !ok || len(field.Index) != 1 { - return 0, fmt.Errorf("dbkit: the SQLite driver connection %T holds no handle in its field db, so a busy wait "+ - "cannot end with its context", conn) - } - f := v.Field(field.Index[0]) - if f.Kind() != reflect.Uintptr || f.Uint() == 0 { - return 0, fmt.Errorf("dbkit: the SQLite driver connection %T holds no handle in its field db, so a busy wait "+ - "cannot end with its context", conn) - } - return uintptr(f.Uint()), nil -} - -// driverConn is every interface the driver's connection offers to database/sql. -type driverConn interface { - driver.Conn - driver.ConnBeginTx - driver.ConnPrepareContext - driver.ExecerContext - driver.QueryerContext - driver.Pinger - driver.SessionResetter - driver.Validator -} - -// waitOn points the busy callback of conn at busyHandler and returns conn wrapped so its waits end with their context. -func (c *connector) waitOn(conn driver.Conn) (driver.Conn, error) { - db, err := handleOf(conn) - if err != nil { - return nil, err - } - full, ok := conn.(driverConn) - if !ok { - return nil, fmt.Errorf("dbkit: the SQLite driver connection %T lacks an interface database/sql uses", conn) - } - id := lastWaitID.Add(1) - wait := &busyWait{db: db, timeout: c.busyTimeout, poll: c.busyPoll} - busyWaits.Store(id, wait) - tls := libc.NewTLS() - defer tls.Close() - sqlite3.Xsqlite3_busy_handler(tls, db, busyHandlerFP, id) - return &busyConn{driverConn: full, id: id, wait: wait}, nil -} - -// busyConn is a driver connection whose waits for a lock end with their context. -type busyConn struct { - // driverConn is the driver's own connection. - driverConn - // id is the key of the connection's wait in busyWaits. - id uintptr - // wait is the connection's wait, the one busyWaits holds under id. - wait *busyWait -} - -// during hands ctx's Done channel to the busy handler for one call and returns what ends that span. -func (w *busyWait) during(ctx context.Context) func() { - w.span.Store(&callSpan{done: ctx.Done()}) - return func() { w.span.Store(nil) } -} - -// BeginTx starts a transaction, and answers the error of ctx alone when it failed after ctx ended. -func (c *busyConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { - defer c.wait.during(ctx)() - tx, err := c.driverConn.BeginTx(ctx, opts) - if err != nil { - return nil, interrupted(ctx, err) - } - return tx, nil -} - -// ExecContext runs a statement, and answers the error of ctx alone when it failed after ctx ended. -func (c *busyConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { - defer c.wait.during(ctx)() - r, err := c.driverConn.ExecContext(ctx, query, args) - if err != nil { - return nil, interrupted(ctx, err) - } - return r, nil -} - -// QueryContext runs a query whose every step waits under ctx, and answers ctx's error alone once it ended. -func (c *busyConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { - return c.wait.query(ctx, func() (driver.Rows, error) { return c.driverConn.QueryContext(ctx, query, args) }) -} - -// query makes one query under a span of ctx, answers ctx's error alone once it ended, and wraps the rows it answers. -func (w *busyWait) query(ctx context.Context, run func() (driver.Rows, error)) (driver.Rows, error) { - defer w.during(ctx)() - rows, err := run() - if err != nil { - return nil, interrupted(ctx, err) - } - if end := ctx.Err(); end != nil { - _ = rows.Close() - return nil, end - } - return w.wrapRows(ctx, rows), nil -} - -// driverRows is every interface the driver's rows offer to database/sql. -type driverRows interface { - driver.Rows - driver.RowsColumnTypeDatabaseTypeName - driver.RowsColumnTypeLength - driver.RowsColumnTypeNullable - driver.RowsColumnTypePrecisionScale - driver.RowsColumnTypeScanType -} - -// wrapRows returns rows whose Next and Close wait under a span of ctx, or rows as they are if they lack an interface. -func (w *busyWait) wrapRows(ctx context.Context, rows driver.Rows) driver.Rows { - full, ok := rows.(driverRows) - if !ok { - return rows - } - return &busyRows{driverRows: full, ctx: ctx, wait: w} -} - -// busyRows is the driver's rows of a query whose every Next and Close waits under a span of the query's context. -type busyRows struct { - // driverRows is the driver's own rows. - driverRows - // ctx is the context of the query. - ctx context.Context - // wait is the wait of the connection that ran the query. - wait *busyWait -} - -// Next reads the next row under the query's context, and answers its error alone when the read failed after it ended. -func (r *busyRows) Next(dest []driver.Value) error { - defer r.wait.during(r.ctx)() - err := r.driverRows.Next(dest) - if err == nil || err == io.EOF { - return err - } - return interrupted(r.ctx, err) -} - -// Close closes the rows under the query's context, and answers its error alone when the close failed after it ended. -func (r *busyRows) Close() error { - defer r.wait.during(r.ctx)() - if err := r.driverRows.Close(); err != nil { - return interrupted(r.ctx, err) - } - return nil -} - -// PrepareContext prepares a statement, and answers the error of ctx alone when it failed after ctx ended. -func (c *busyConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { - defer c.wait.during(ctx)() - stmt, err := c.driverConn.PrepareContext(ctx, query) - if err != nil { - return nil, interrupted(ctx, err) - } - return c.wait.wrapStmt(stmt), nil -} - -// driverStmt is every interface the driver's statement offers to database/sql. -type driverStmt interface { - driver.Stmt - driver.StmtExecContext - driver.StmtQueryContext -} - -// wrapStmt returns stmt whose every run waits under a span of its context, or stmt as it is if it lacks an interface. -func (w *busyWait) wrapStmt(stmt driver.Stmt) driver.Stmt { - full, ok := stmt.(driverStmt) - if !ok { - return stmt - } - return &busyStmt{driverStmt: full, wait: w} -} - -// busyStmt is a driver statement whose every run waits under a span of the run's context. -type busyStmt struct { - // driverStmt is the driver's own statement. - driverStmt - // wait is the wait of the connection that prepared the statement. - wait *busyWait -} - -// ExecContext runs the statement, and answers the error of ctx alone when it failed after ctx ended. -func (s *busyStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { - defer s.wait.during(ctx)() - r, err := s.driverStmt.ExecContext(ctx, args) - if err != nil { - return nil, interrupted(ctx, err) - } - return r, nil -} - -// QueryContext runs the statement as a query whose steps wait under ctx, and answers ctx's error alone once it ended. -func (s *busyStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { - return s.wait.query(ctx, func() (driver.Rows, error) { return s.driverStmt.QueryContext(ctx, args) }) -} - -// Ping checks the connection, and answers the error of ctx alone when it failed after ctx ended. -func (c *busyConn) Ping(ctx context.Context) error { - defer c.wait.during(ctx)() - if err := c.driverConn.Ping(ctx); err != nil { - return interrupted(ctx, err) - } - return nil -} - -// Close closes the connection and forgets its wait. -func (c *busyConn) Close() error { - err := c.driverConn.Close() - busyWaits.Delete(c.id) - return err -} - -// ended returns err marked with the error of ctx once ctx has ended, and err as it is before. -func ended(ctx context.Context, err error) error { - if end := ctx.Err(); end != nil { - return fmt.Errorf("%w: %w", err, end) - } - return err -} - -// interrupted returns the error of ctx in place of err once ctx has ended, and err as it is before. -func interrupted(ctx context.Context, err error) error { - if end := ctx.Err(); end != nil { - return end - } - return err -} diff --git a/dbkit/sqlite/busy_calls_internal_test.go b/dbkit/sqlite/busy_calls_internal_test.go deleted file mode 100644 index 269e720..0000000 --- a/dbkit/sqlite/busy_calls_internal_test.go +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package sqlite - -import ( - "context" - "database/sql/driver" - "testing" -) - -const ( - // internalInputs is the count of placeholders the fake statement answers. - internalInputs = 3 - // internalAffected is the count of rows the fake statement's run answers. - internalAffected = 4 -) - -// internalSpanConn is a driver connection that records the Done channel the wait's span holds during each call. -type internalSpanConn struct { - // driverConn is left nil. - driverConn - // wait is the wait whose span each call reads. - wait *busyWait - // seen maps each call to the Done channel of the span it ran in, nil for a call with no span. - seen map[string]<-chan struct{} - // rows are the rows every query of the connection answers. - rows *internalSpanRows - // stmtRows are the rows every query of a statement the connection prepared answers. - stmtRows *internalSpanRows -} - -// record notes the Done channel of the span the call name runs in. -func (c *internalSpanConn) record(name string) { - c.seen[name] = internalSpanDone(c.wait) -} - -// BeginTx records its span and starts no transaction. -func (c *internalSpanConn) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { - c.record("BeginTx") - return nil, errStepFailed -} - -// ExecContext records its span and runs nothing. -func (c *internalSpanConn) ExecContext(context.Context, string, []driver.NamedValue) (driver.Result, error) { - c.record("ExecContext") - return nil, errStepFailed -} - -// QueryContext records its span and answers the rows of the connection. -func (c *internalSpanConn) QueryContext(context.Context, string, []driver.NamedValue) (driver.Rows, error) { - c.record("QueryContext") - return c.rows, nil -} - -// PrepareContext records its span and answers a statement that records the span of each run. -func (c *internalSpanConn) PrepareContext(context.Context, string) (driver.Stmt, error) { - c.record("PrepareContext") - return &internalSpanStmt{conn: c}, nil -} - -// Ping records its span. -func (c *internalSpanConn) Ping(context.Context) error { - c.record("Ping") - return nil -} - -// internalSpanStmt is a driver statement that records the span of each run on the connection that prepared it. -type internalSpanStmt struct { - // Stmt is left nil. - driver.Stmt - // conn is the connection that prepared the statement. - conn *internalSpanConn -} - -// ExecContext records its span and runs nothing. -func (s *internalSpanStmt) ExecContext(context.Context, []driver.NamedValue) (driver.Result, error) { - s.conn.record("Stmt.ExecContext") - return nil, errStepFailed -} - -// QueryContext records its span and answers the statement rows of the connection. -func (s *internalSpanStmt) QueryContext(context.Context, []driver.NamedValue) (driver.Rows, error) { - s.conn.record("Stmt.QueryContext") - return s.conn.stmtRows, nil -} - -// internalContexts returns a live context for each name, each with a Done channel of its own. -func internalContexts(t *testing.T, names ...string) map[string]context.Context { - t.Helper() - ctxs := make(map[string]context.Context, len(names)) - for _, name := range names { - ctx, cancel := context.WithCancel(t.Context()) - t.Cleanup(cancel) - ctxs[name] = ctx - } - return ctxs -} - -// internalReadAndClose reads one row of rows and closes them when the query answered no error. -func internalReadAndClose(rows driver.Rows, err error) { - if err == nil { - _ = rows.Next(nil) - _ = rows.Close() - } -} - -// internalWrappedCall is one call through a wrapped connection, made under the context of its name. -type internalWrappedCall struct { - // name names the call. - name string - // run makes the call under ctx. - run func(ctx context.Context) -} - -// internalCallsOf returns every call of conn and of the statement it prepares, in the order the test makes them. -func internalCallsOf(conn *busyConn) []internalWrappedCall { - var stmt driver.Stmt - return []internalWrappedCall{ - {"BeginTx", func(ctx context.Context) { _, _ = conn.BeginTx(ctx, driver.TxOptions{}) }}, - {"ExecContext", func(ctx context.Context) { _, _ = conn.ExecContext(ctx, internalOptimize, nil) }}, - {"QueryContext", func(ctx context.Context) { internalReadAndClose(conn.QueryContext(ctx, internalOptimize, nil)) }}, - {"PrepareContext", func(ctx context.Context) { stmt, _ = conn.PrepareContext(ctx, internalOptimize) }}, - {"Stmt.ExecContext", func(ctx context.Context) { _, _ = stmt.(driver.StmtExecContext).ExecContext(ctx, nil) }}, - {"Stmt.QueryContext", func(ctx context.Context) { - internalReadAndClose(stmt.(driver.StmtQueryContext).QueryContext(ctx, nil)) - }}, - {"Ping", func(ctx context.Context) { _ = conn.Ping(ctx) }}, - } -} - -// internalRunCalls makes each call under its context in ctxs and fails the test when a span stays after a call. -func internalRunCalls(t *testing.T, wait *busyWait, calls []internalWrappedCall, ctxs map[string]context.Context) { - t.Helper() - for _, call := range calls { - call.run(ctxs[call.name]) - if s := wait.span.Load(); s != nil { - t.Errorf("the span after %s = %+v, want none between calls", call.name, s) - } - } -} - -// internalMustReadUnder fails the test unless each Next and Close of rows ran under the Done channel of ctx. -func internalMustReadUnder(t *testing.T, name string, rows *internalSpanRows, ctx context.Context) { - t.Helper() - if rows.nextUnder != ctx.Done() || rows.closedUnder != ctx.Done() { - t.Errorf("the span during Next and Close of the rows of %s holds %v and %v, want the Done channel %v of %s", - name, rows.nextUnder, rows.closedUnder, ctx.Done(), name) - } -} - -func TestEveryCallHandsItsDoneChannelToTheWait(t *testing.T) { - t.Parallel() - - wait := &busyWait{} - fake := &internalSpanConn{wait: wait, seen: map[string]<-chan struct{}{}, rows: &internalSpanRows{wait: wait}, - stmtRows: &internalSpanRows{wait: wait}} - conn := &busyConn{driverConn: fake, wait: wait} - ctxs := internalContexts(t, "BeginTx", "ExecContext", "QueryContext", "PrepareContext", "Ping", - "Stmt.ExecContext", "Stmt.QueryContext") - - internalRunCalls(t, wait, internalCallsOf(conn), ctxs) - - for name, ctx := range ctxs { - if done, ok := fake.seen[name]; !ok || done != ctx.Done() { - t.Errorf("the span during %s holds %v, want the Done channel %v of the call's own context", name, done, - ctx.Done()) - } - } - internalMustReadUnder(t, "QueryContext", fake.rows, ctxs["QueryContext"]) - internalMustReadUnder(t, "Stmt.QueryContext", fake.stmtRows, ctxs["Stmt.QueryContext"]) -} - -// internalStmtConn is a driver connection whose every prepare answers its statement. -type internalStmtConn struct { - // driverConn is left nil. - driverConn - // stmt is the statement every prepare answers. - stmt driver.Stmt -} - -// PrepareContext answers the statement of the connection. -func (c *internalStmtConn) PrepareContext(context.Context, string) (driver.Stmt, error) { - return c.stmt, nil -} - -// internalStmt is a driver statement whose every call answers a value of its own and whose Close is recorded. -type internalStmt struct { - // Stmt is left nil. - driver.Stmt - // closed reports that Close ran. - closed bool -} - -// NumInput answers internalInputs. -func (*internalStmt) NumInput() int { - return internalInputs -} - -// Close records that it ran and fails with errCloseFailed. -func (s *internalStmt) Close() error { - s.closed = true - return errCloseFailed -} - -// ExecContext answers internalAffected rows affected. -func (*internalStmt) ExecContext(context.Context, []driver.NamedValue) (driver.Result, error) { - return driver.RowsAffected(internalAffected), nil -} - -// QueryContext answers rows that answer every column type question. -func (*internalStmt) QueryContext(context.Context, []driver.NamedValue) (driver.Rows, error) { - return &internalTypedRows{}, nil -} - -// internalPlainStmt is a driver statement with only the methods of driver.Stmt. -type internalPlainStmt struct { - // Stmt is left nil. - driver.Stmt -} - -// internalPrepare prepares a statement through a wrapper whose driver answers stmt, and returns what it answered. -func internalPrepare(t *testing.T, stmt driver.Stmt) driver.Stmt { - t.Helper() - conn := &busyConn{driverConn: &internalStmtConn{stmt: stmt}, wait: &busyWait{}} - got, err := conn.PrepareContext(t.Context(), internalOptimize) - if err != nil { - t.Fatalf("PrepareContext() error = %v, want nil", err) - } - return got -} - -// internalMustRunTheStatement fails the test unless stmt runs and answers the results of internalStmt. -func internalMustRunTheStatement(t *testing.T, stmt driverStmt) { - t.Helper() - result, err := stmt.ExecContext(t.Context(), nil) - if err != nil { - t.Fatalf("ExecContext() error = %v, want nil", err) - } - if n, err := result.RowsAffected(); n != internalAffected || err != nil { - t.Errorf("RowsAffected() = %d, %v, want the driver's %d", n, err, internalAffected) - } - rows, err := stmt.QueryContext(t.Context(), nil) - if err != nil { - t.Fatalf("QueryContext() error = %v, want nil", err) - } - for _, p := range internalColumnProbes { - if !p.answers(rows) { - t.Errorf("%s on the rows of a prepared query is missing or answers otherwise, want the driver's", p.name) - } - } -} - -func TestAPreparedStatementKeepsEveryInterfaceOfTheDriversStatement(t *testing.T) { - t.Parallel() - - fake := &internalStmt{} - stmt, ok := internalPrepare(t, fake).(driverStmt) - if !ok { - t.Fatalf("PrepareContext() answers %T, want a statement with every interface of driverStmt", stmt) - } - - internalMustRunTheStatement(t, stmt) - - if n := stmt.NumInput(); n != internalInputs { - t.Errorf("NumInput() = %d, want the driver's %d", n, internalInputs) - } - if err := stmt.Close(); err != errCloseFailed || !fake.closed { - t.Errorf("Close() error = %v and closed = %v, want the driver's statement closed and its error as it is", err, - fake.closed) - } -} - -func TestAStatementLackingAnInterfaceIsPreparedAsItIs(t *testing.T) { - t.Parallel() - - plain := &internalPlainStmt{} - - if got := internalPrepare(t, plain); got != driver.Stmt(plain) { - t.Errorf("PrepareContext() = %T, want the driver's statement %T as it is", got, plain) - } -} diff --git a/dbkit/sqlite/busy_internal_test.go b/dbkit/sqlite/busy_internal_test.go deleted file mode 100644 index eb690ff..0000000 --- a/dbkit/sqlite/busy_internal_test.go +++ /dev/null @@ -1,775 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package sqlite - -import ( - "cmp" - "context" - "database/sql/driver" - "errors" - "fmt" - "io" - "path/filepath" - "reflect" - "testing" - "time" - - "modernc.org/libc" - sqlite3 "modernc.org/sqlite/lib" -) - -const ( - // internalCount is a busy count past the first call of a wait. - internalCount = 7 - // internalFakeHandle is a handle number no SQLite connection has. - internalFakeHandle = 1 - // internalUpstreamMethod is the method a driver connection gains with an upstream busy handler. - internalUpstreamMethod = "RegisterBusyHandler" - // internalKnownMethod is a method of the driver connection the lookup must find. - internalKnownMethod = "Ping" - // internalTypeName is the database type name the typed rows answer. - internalTypeName = "DECIMAL" - // internalLength is the column length the typed rows answer. - internalLength = 20 - // internalPrecision is the precision the typed rows answer. - internalPrecision = 10 - // internalScale is the scale the typed rows answer. - internalScale = 2 -) - -func TestEndedMarksAnErrorOnlyOnceItsContextEnded(t *testing.T) { - t.Parallel() - - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - - if got := ended(t.Context(), errStepFailed); got != errStepFailed { - t.Errorf("ended() under a live context = %v, want the error as it is", got) - } - got := ended(cancelled, errStepFailed) - if !errors.Is(got, errStepFailed) || !errors.Is(got, context.Canceled) { - t.Errorf("ended() under a cancelled context = %v, want an error matching both the error and context.Canceled", - got) - } - if want := errStepFailed.Error() + ": " + context.Canceled.Error(); got == nil || got.Error() != want { - t.Errorf("ended() under a cancelled context = %v, want %q", got, want) - } -} - -// internalHandleConn is a driver connection with a field db and only the methods of internalConn. -type internalHandleConn struct { - // internalConn gives the connection the methods of driver.Conn and ExecContext. - internalConn - // db is the handle number the connection holds. - db uintptr -} - -// internalEmbeddedConn is a driver connection whose field db comes through an embedded pointer. -type internalEmbeddedConn struct { - // internalHandleConn gives the connection a field db, left nil. - *internalHandleConn -} - -// internalWrongHandleConn is a driver connection whose field db is no number. -type internalWrongHandleConn struct { - // internalConn gives the connection the methods of driver.Conn and ExecContext. - internalConn - // db is a handle of the wrong type. - db string -} - -// internalNumberConn is a driver connection that is a number, not a struct. -type internalNumberConn int - -// Prepare refuses every statement. -func (internalNumberConn) Prepare(string) (driver.Stmt, error) { - return nil, errors.New("busy test: no statement is prepared on an internalNumberConn") -} - -// Close does nothing. -func (internalNumberConn) Close() error { - return nil -} - -// Begin refuses every transaction. -func (internalNumberConn) Begin() (driver.Tx, error) { - return nil, errors.New("busy test: no transaction runs on an internalNumberConn") -} - -// internalNoHandle returns the error text of handleOf for a connection of the type named kind with no handle. -func internalNoHandle(kind string) string { - return "dbkit: the SQLite driver connection " + kind + - " holds no handle in its field db, so a busy wait cannot end with its context" -} - -func TestHandleOfRefusesAConnectionWithoutAHandle(t *testing.T) { - t.Parallel() - - cases := []struct { - // name names the connection. - name string - // conn is the connection handleOf reads. - conn driver.Conn - // want is the error handleOf answers. - want string - }{ - {"a struct with no field db", &internalConn{}, internalNoHandle("*sqlite.internalConn")}, - {"a field db of another type", &internalWrongHandleConn{db: "site.db"}, - internalNoHandle("*sqlite.internalWrongHandleConn")}, - {"a field db of zero", &internalHandleConn{}, internalNoHandle("*sqlite.internalHandleConn")}, - {"a field db behind an embedded pointer", &internalEmbeddedConn{}, internalNoHandle("*sqlite.internalEmbeddedConn")}, - {"a value that is no struct", internalNumberConn(0), - "dbkit: the SQLite driver connection sqlite.internalNumberConn is not a struct, so a busy wait cannot end " + - "with its context"}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - - db, err := handleOf(c.conn) - - if db != 0 { - t.Errorf("handleOf() handle = %d, want 0", db) - } - if got := fmt.Sprint(err); got != c.want { - t.Errorf("handleOf() error = %s, want %q", got, c.want) - } - }) - } -} - -func TestConnectFailsClosedWithoutAHandle(t *testing.T) { - t.Parallel() - - fake := &internalConn{} - c, _ := internalConnector(fake, nil) - c.wait = c.waitOn - - conn, err := c.Connect(t.Context()) - - if conn != nil { - t.Errorf("Connect() connection = %v, want nil", conn) - } - if got, want := fmt.Sprint(err), internalNoHandle("*sqlite.internalConn"); got != want { - t.Errorf("Connect() error = %s, want %q", got, want) - } - if !fake.closed { - t.Errorf("closed = %v, want the connection closed", fake.closed) - } -} - -// internalWaitHolds reports whether busyWaits holds a wait on the handle db. -func internalWaitHolds(db uintptr) bool { - held := false - busyWaits.Range(func(_, v any) bool { - held = v.(*busyWait).db == db - return !held - }) - return held -} - -func TestWaitOnRefusesAConnectionMissingAnInterface(t *testing.T) { - t.Parallel() - - c, _ := internalConnector(nil, nil) - - conn, err := c.waitOn(&internalHandleConn{db: internalFakeHandle}) - - want := "dbkit: the SQLite driver connection *sqlite.internalHandleConn lacks an interface database/sql uses" - if conn != nil { - t.Errorf("waitOn() connection = %v, want nil", conn) - } - if got := fmt.Sprint(err); got != want { - t.Errorf("waitOn() error = %s, want %q", got, want) - } - if internalWaitHolds(internalFakeHandle) { - t.Errorf("busyWaits holds a wait on the handle %d, want SQLite never handed the number", internalFakeHandle) - } -} - -// internalRealConn returns a connection Connect opened to a fresh file, closed when the test ends. -func internalRealConn(t *testing.T) *busyConn { - t.Helper() - if err := ofdOnce(); err != nil { - t.Fatalf("ofdOnce() error = %v, want nil", err) - } - c := newConnector(internalDriver(t), filepath.Join(t.TempDir(), "site.db"), internalOptions()) - conn, err := c.Connect(t.Context()) - if err != nil { - t.Fatalf("Connect() error = %v, want nil", err) - } - t.Cleanup(func() { _ = conn.Close() }) - waiting, ok := conn.(*busyConn) - if !ok { - t.Fatalf("Connect() = %T, want a *busyConn", conn) - } - return waiting -} - -// internalHandler calls busyHandler with a fresh libc TLS for the wait of id at count. -func internalHandler(id uintptr, count int32) int32 { - tls := libc.NewTLS() - defer tls.Close() - return busyHandler(tls, id, count) -} - -func TestAClosedConnectionForgetsItsWait(t *testing.T) { - t.Parallel() - - conn := internalRealConn(t) - if _, ok := busyWaits.Load(conn.id); !ok { - t.Fatalf("busyWaits holds no wait for the open connection %d, want its wait", conn.id) - } - - if err := conn.Close(); err != nil { - t.Fatalf("Close() error = %v, want nil", err) - } - - if _, ok := busyWaits.Load(conn.id); ok { - t.Errorf("busyWaits still holds the wait of the closed connection %d, want it forgotten", conn.id) - } - if got := internalHandler(conn.id, 0); got != 0 { - t.Errorf("busyHandler() for a forgotten wait = %d, want 0 to answer busy", got) - } -} - -func TestAWaitThatStartsAboveCountZeroStartsFresh(t *testing.T) { - t.Parallel() - - conn := internalRealConn(t) - conn.wait.began = time.Now().Add(-internalUnreachedWait) - conn.wait.span.Store(&callSpan{}) - - got := internalHandler(conn.id, internalCount) - - if got != 1 { - t.Errorf("busyHandler() at count %d of a call's first wait = %d, want 1 to try the lock again", internalCount, - got) - } -} - -func TestAWaitOutsideACallKeepsItsStartPastCountZero(t *testing.T) { - t.Parallel() - - conn := internalRealConn(t) - conn.wait.began = time.Now().Add(-internalUnreachedWait) - - got := internalHandler(conn.id, internalCount) - - if got != 0 { - t.Errorf("busyHandler() at count %d of a wait an hour old = %d, want 0 to answer busy", internalCount, got) - } -} - -func TestAWaitOutsideACallStartsFreshAtCountZero(t *testing.T) { - t.Parallel() - - conn := internalRealConn(t) - conn.wait.began = time.Now().Add(-internalUnreachedWait) - - got := internalHandler(conn.id, 0) - - if got != 1 { - t.Errorf("busyHandler() at count 0 after a wait an hour old = %d, want 1 to try the lock again", got) - } -} - -func TestAnInterruptedHandleAnswersBusyDuringACallWhoseContextIsLive(t *testing.T) { - t.Parallel() - - conn := internalRealConn(t) - defer conn.wait.during(t.Context())() - tls := libc.NewTLS() - defer tls.Close() - sqlite3.Xsqlite3_interrupt(tls, conn.wait.db) - - got := internalHandler(conn.id, 0) - - if got != 0 { - t.Errorf("busyHandler() at count 0 on an interrupted handle during a live call = %d, want 0 to answer busy", got) - } -} - -// internalBusyConn is a driver connection whose every call answers a busy error. -type internalBusyConn struct { - // driverConn is left nil. - driverConn - // busy is the error every call answers with. - busy error -} - -// BeginTx answers the busy error. -func (c *internalBusyConn) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { - return nil, c.busy -} - -// ExecContext answers the busy error. -func (c *internalBusyConn) ExecContext(context.Context, string, []driver.NamedValue) (driver.Result, error) { - return nil, c.busy -} - -// QueryContext answers the busy error. -func (c *internalBusyConn) QueryContext(context.Context, string, []driver.NamedValue) (driver.Rows, error) { - return nil, c.busy -} - -// PrepareContext answers the busy error. -func (c *internalBusyConn) PrepareContext(context.Context, string) (driver.Stmt, error) { - return nil, c.busy -} - -// Ping answers the busy error. -func (c *internalBusyConn) Ping(context.Context) error { - return c.busy -} - -// internalStatementCalls returns each statement call of conn as a function of its context. -func internalStatementCalls(conn *busyConn) map[string]func(ctx context.Context) error { - return map[string]func(ctx context.Context) error{ - "ExecContext": func(ctx context.Context) error { - _, err := conn.ExecContext(ctx, internalOptimize, nil) - return err - }, - "QueryContext": func(ctx context.Context) error { - _, err := conn.QueryContext(ctx, internalOptimize, nil) - return err - }, - "PrepareContext": func(ctx context.Context) error { - _, err := conn.PrepareContext(ctx, internalOptimize) - return err - }, - "Ping": conn.Ping, - } -} - -func TestAStatementAfterItsContextEndedAnswersTheContextError(t *testing.T) { - t.Parallel() - - busy := internalBusyError(t) - conn := &busyConn{driverConn: &internalBusyConn{busy: busy}, wait: &busyWait{}} - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - - for name, call := range internalStatementCalls(conn) { - if err := call(cancelled); err != context.Canceled { - t.Errorf("%s under a cancelled context error = %v, want exactly context.Canceled", name, err) - } - if err := call(t.Context()); err != busy { - t.Errorf("%s under a live context error = %v, want the busy error as it is", name, err) - } - } -} - -func TestABeginAfterItsContextEndedAnswersTheContextError(t *testing.T) { - t.Parallel() - - busy := internalBusyError(t) - conn := &busyConn{driverConn: &internalBusyConn{busy: busy}, wait: &busyWait{}} - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - - _, err := conn.BeginTx(cancelled, driver.TxOptions{}) - _, live := conn.BeginTx(t.Context(), driver.TxOptions{}) - - if err != context.Canceled { - t.Errorf("BeginTx() under a cancelled context error = %v, want exactly context.Canceled", err) - } - if live != busy { - t.Errorf("BeginTx() under a live context error = %v, want the busy error as it is", live) - } -} - -// internalRows is a result set with no rows that records its Close. -type internalRows struct { - // closed reports that Close ran. - closed bool -} - -// Columns names no column. -func (*internalRows) Columns() []string { - return nil -} - -// Close records that it ran. -func (r *internalRows) Close() error { - r.closed = true - return nil -} - -// Next answers the end of the rows. -func (*internalRows) Next([]driver.Value) error { - return io.EOF -} - -// internalRowsConn is a driver connection whose every query answers its rows. -type internalRowsConn struct { - // driverConn is left nil. - driverConn - // rows is the result set every query answers. - rows driver.Rows -} - -// QueryContext answers the rows of the connection. -func (c *internalRowsConn) QueryContext(context.Context, string, []driver.NamedValue) (driver.Rows, error) { - return c.rows, nil -} - -// internalQuery runs a query under ctx through a wrapper whose driver answers rows, and returns what it answered. -func internalQuery(ctx context.Context, rows *internalRows) (driver.Rows, error) { - conn := &busyConn{driverConn: &internalRowsConn{rows: rows}, wait: &busyWait{}} - return conn.QueryContext(ctx, internalOptimize, nil) -} - -func TestAQueryThatAnswersAfterItsContextEndedAnswersTheContextError(t *testing.T) { - t.Parallel() - - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - ended := &internalRows{} - - rows, err := internalQuery(cancelled, ended) - - if rows != nil || err != context.Canceled { - t.Errorf("QueryContext() under a cancelled context = %v, %v, want no rows and exactly context.Canceled", rows, err) - } - if !ended.closed { - t.Errorf("closed = %v, want the rows of a query whose context ended closed", ended.closed) - } -} - -func TestAQueryUnderALiveContextAnswersItsRows(t *testing.T) { - t.Parallel() - - live := &internalRows{} - - rows, err := internalQuery(t.Context(), live) - - if rows != driver.Rows(live) || err != nil { - t.Errorf("QueryContext() under a live context = %v, %v, want its rows and nil", rows, err) - } - if live.closed { - t.Errorf("closed = %v, want the rows of a live query left open", live.closed) - } -} - -// internalTypedRows is a result set with no rows that answers each column type question with a value of its own. -type internalTypedRows struct { - // internalRows gives the rows Columns, Next and a recorded Close. - internalRows -} - -// ColumnTypeDatabaseTypeName answers internalTypeName. -func (*internalTypedRows) ColumnTypeDatabaseTypeName(int) string { - return internalTypeName -} - -// ColumnTypeLength answers internalLength. -func (*internalTypedRows) ColumnTypeLength(int) (int64, bool) { - return internalLength, true -} - -// ColumnTypeNullable answers that the column holds no NULL. -func (*internalTypedRows) ColumnTypeNullable(int) (bool, bool) { - return false, true -} - -// ColumnTypePrecisionScale answers internalPrecision and internalScale. -func (*internalTypedRows) ColumnTypePrecisionScale(int) (int64, int64, bool) { - return internalPrecision, internalScale, true -} - -// ColumnTypeScanType answers the type of a bool. -func (*internalTypedRows) ColumnTypeScanType(int) reflect.Type { - return reflect.TypeFor[bool]() -} - -// internalTypedQuery returns a wrapper on wait whose driver answers typed to every query. -func internalTypedQuery(wait *busyWait, typed *internalTypedRows) *busyConn { - return &busyConn{driverConn: &internalRowsConn{rows: typed}, wait: wait} -} - -// internalSpanDone returns the Done channel the span of wait holds, nil when it holds no span. -func internalSpanDone(wait *busyWait) <-chan struct{} { - if s := wait.span.Load(); s != nil { - return s.done - } - return nil -} - -// internalSpanRows is typed rows whose Next and Close record the Done channel the span of wait holds. -type internalSpanRows struct { - // internalTypedRows answers every column type question. - internalTypedRows - // wait is the wait whose span Next and Close record. - wait *busyWait - // nextUnder is the Done channel the span held when Next last ran. - nextUnder <-chan struct{} - // closedUnder is the Done channel the span held when Close ran. - closedUnder <-chan struct{} -} - -// Next records the Done channel the span of wait holds and answers the end of the rows. -func (r *internalSpanRows) Next([]driver.Value) error { - r.nextUnder = internalSpanDone(r.wait) - return io.EOF -} - -// Close records the Done channel the span of wait holds and fails with errStepFailed. -func (r *internalSpanRows) Close() error { - r.closedUnder = internalSpanDone(r.wait) - return errStepFailed -} - -func TestTheRowsOfAQueryHoldItsSpanOnlyDuringNextAndClose(t *testing.T) { - t.Parallel() - - wait := &busyWait{} - spanRows := &internalSpanRows{wait: wait} - ctx := t.Context() - - rows, err := (&busyConn{driverConn: &internalRowsConn{rows: spanRows}, wait: wait}).QueryContext(ctx, - internalOptimize, nil) - if err != nil { - t.Fatalf("QueryContext() error = %v, want nil", err) - } - open := wait.span.Load() - next := rows.Next(nil) - between := wait.span.Load() - closed := rows.Close() - - if open != nil || between != nil { - t.Errorf("the span while the rows are open between calls = %+v, then %+v, want none", open, between) - } - internalMustReadUnder(t, "the query", spanRows, ctx) - if next != io.EOF || closed != errStepFailed { - t.Errorf("Next() then Close() error = %v, then %v, want the driver's %v, then %v", next, closed, io.EOF, - errStepFailed) - } - if s := wait.span.Load(); s != nil { - t.Errorf("the span after Close = %+v, want none", s) - } -} - -func TestANextAfterAnotherCallStillWaitsUnderTheContextOfItsQuery(t *testing.T) { - t.Parallel() - - wait := &busyWait{} - spanRows := &internalSpanRows{wait: wait} - queries := &busyConn{driverConn: &internalRowsConn{rows: spanRows}, wait: wait} - execs := &busyConn{driverConn: &internalBusyConn{busy: errStepFailed}, wait: wait} - ctxs := internalContexts(t, "the query", "the statement") - rows, err := queries.QueryContext(ctxs["the query"], internalOptimize, nil) - if err != nil { - t.Fatalf("QueryContext() error = %v, want nil", err) - } - - _, _ = execs.ExecContext(ctxs["the statement"], internalOptimize, nil) - _ = rows.Next(nil) - _ = rows.Close() - - internalMustReadUnder(t, "the query", spanRows, ctxs["the query"]) - if s := wait.span.Load(); s != nil { - t.Errorf("the span after Close = %+v, want none", s) - } -} - -func TestEveryQueryEndsItsSpanBeforeItAnswers(t *testing.T) { - t.Parallel() - - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - cases := []struct { - // name names the query. - name string - // ctx is the context of the query. - ctx context.Context - // driverConn is the driver connection that answers the query. - driverConn driverConn - }{ - {"a query the driver failed", t.Context(), &internalBusyConn{busy: errStepFailed}}, - {"a query whose context ended", cancelled, &internalRowsConn{rows: &internalTypedRows{}}}, - {"rows lacking a column type interface", t.Context(), &internalRowsConn{rows: &internalRows{}}}, - {"rows the wrapper keeps", t.Context(), &internalRowsConn{rows: &internalTypedRows{}}}, - } - for _, c := range cases { - wait := &busyWait{} - - rows, _ := (&busyConn{driverConn: c.driverConn, wait: wait}).QueryContext(c.ctx, internalOptimize, nil) - - if s := wait.span.Load(); s != nil { - t.Errorf("the span after %s = %+v, want none", c.name, s) - } - if rows != nil { - _ = rows.Close() - } - } -} - -func TestTheDriversRowsCloseUnderTheSpanOfTheirQuery(t *testing.T) { - t.Parallel() - - cancelled, cancel := context.WithCancel(t.Context()) - cancel() - for _, ctx := range []context.Context{t.Context(), cancelled} { - wait := &busyWait{} - spanRows := &internalSpanRows{wait: wait} - conn := &busyConn{driverConn: &internalRowsConn{rows: spanRows}, wait: wait} - - rows, err := conn.QueryContext(ctx, internalOptimize, nil) - if err == nil { - err = rows.Close() - } - - if spanRows.closedUnder != ctx.Done() { - t.Errorf("the span when the driver's rows closed held %v, want the Done channel %v of their query", - spanRows.closedUnder, ctx.Done()) - } - if want := cmp.Or(ctx.Err(), errStepFailed); err != want { - t.Errorf("QueryContext() then Close() error = %v, want %v as it is", err, want) - } - if s := wait.span.Load(); s != nil { - t.Errorf("the span after the driver's rows closed = %+v, want none", s) - } - } -} - -// internalColumnProbes asks rows each column type question and reports whether they answer as internalTypedRows. -var internalColumnProbes = []struct { - // name names the interface. - name string - // answers reports whether rows offer the interface and answer it as internalTypedRows. - answers func(rows driver.Rows) bool -}{ - {"driver.RowsColumnTypeDatabaseTypeName", func(rows driver.Rows) bool { - r, ok := rows.(driver.RowsColumnTypeDatabaseTypeName) - return ok && r.ColumnTypeDatabaseTypeName(0) == internalTypeName - }}, - {"driver.RowsColumnTypeLength", func(rows driver.Rows) bool { - r, ok := rows.(driver.RowsColumnTypeLength) - if !ok { - return false - } - length, known := r.ColumnTypeLength(0) - return known && length == internalLength - }}, - {"driver.RowsColumnTypeNullable", func(rows driver.Rows) bool { - r, ok := rows.(driver.RowsColumnTypeNullable) - if !ok { - return false - } - nullable, known := r.ColumnTypeNullable(0) - return known && !nullable - }}, - {"driver.RowsColumnTypePrecisionScale", func(rows driver.Rows) bool { - r, ok := rows.(driver.RowsColumnTypePrecisionScale) - if !ok { - return false - } - precision, scale, known := r.ColumnTypePrecisionScale(0) - return known && precision == internalPrecision && scale == internalScale - }}, - {"driver.RowsColumnTypeScanType", func(rows driver.Rows) bool { - r, ok := rows.(driver.RowsColumnTypeScanType) - return ok && r.ColumnTypeScanType(0) == reflect.TypeFor[bool]() - }}, -} - -func TestTheRowsOfAQueryKeepEveryColumnTypeInterface(t *testing.T) { - t.Parallel() - - rows, err := internalTypedQuery(&busyWait{}, &internalTypedRows{}).QueryContext(t.Context(), internalOptimize, nil) - if err != nil { - t.Fatalf("QueryContext() error = %v, want nil", err) - } - defer func() { _ = rows.Close() }() - - for _, p := range internalColumnProbes { - if !p.answers(rows) { - t.Errorf("%s on the rows of a query is missing or answers otherwise, want the driver's answer", p.name) - } - } -} - -// internalDriverConn returns a connection the driver itself opened to a fresh file, closed when the test ends. -func internalDriverConn(t *testing.T) driver.Conn { - t.Helper() - if err := ofdOnce(); err != nil { - t.Fatalf("ofdOnce() error = %v, want nil", err) - } - conn, err := internalDriver(t).Open(filepath.Join(t.TempDir(), "site.db")) - if err != nil { - t.Fatalf("Open() error = %v, want nil", err) - } - t.Cleanup(func() { _ = conn.Close() }) - return conn -} - -// internalOffers reports whether the type of conn has the method name, pointer receivers included. -func internalOffers(conn driver.Conn, name string) bool { - _, ok := reflect.TypeOf(conn).MethodByName(name) - return ok -} - -func TestTheDriverOffersNoBusyHandlerYet(t *testing.T) { - t.Parallel() - - raw := internalDriverConn(t) - if _, err := handleOf(raw); err != nil { - t.Fatalf("handleOf(%T) error = %v, want the driver's own connection", raw, err) - } - if !internalOffers(raw, internalKnownMethod) { - t.Fatalf("the lookup finds no %s on %T, want it to see every method of the driver connection", - internalKnownMethod, raw) - } - - if internalOffers(raw, internalUpstreamMethod) { - t.Errorf("modernc.org/sqlite now offers %s on %T, so replace the busy handler bridge in busy.go with it", - internalUpstreamMethod, raw) - } -} - -func TestTheDriversRowsOfferEveryInterfaceTheWrapperKeepsAndNoOther(t *testing.T) { - t.Parallel() - - rows, err := internalDriverConn(t).(driver.QueryerContext).QueryContext(t.Context(), "SELECT 1", nil) - if err != nil { - t.Fatalf("QueryContext() error = %v, want nil", err) - } - defer func() { _ = rows.Close() }() - - if _, ok := rows.(driverRows); !ok { - t.Errorf("the driver's rows %T lack an interface of driverRows, so every query answers them unwrapped", rows) - } - if _, ok := rows.(driver.RowsNextResultSet); ok { - t.Errorf("modernc.org/sqlite now offers driver.RowsNextResultSet on %T, so keep it in busyRows", rows) - } - if _, ok := rows.(driver.RowsColumnScanner); ok { - t.Errorf("modernc.org/sqlite now offers driver.RowsColumnScanner on %T, so keep it in busyRows", rows) - } -} - -// internalColumnConverter is the one method of driver.ColumnConverter. -type internalColumnConverter interface { - // ColumnConverter returns the converter of the column at idx. - ColumnConverter(idx int) driver.ValueConverter -} - -func TestTheDriversStatementOffersEveryInterfaceTheWrapperKeepsAndNoOther(t *testing.T) { - t.Parallel() - - stmt, err := internalDriverConn(t).(driver.ConnPrepareContext).PrepareContext(t.Context(), "SELECT 1") - if err != nil { - t.Fatalf("PrepareContext() error = %v, want nil", err) - } - defer func() { _ = stmt.Close() }() - - if _, ok := stmt.(driverStmt); !ok { - t.Errorf("the driver's statement %T lacks an interface of driverStmt, so every prepare answers it unwrapped", stmt) - } - if _, ok := stmt.(driver.NamedValueChecker); ok { - t.Errorf("modernc.org/sqlite now offers driver.NamedValueChecker on %T, so keep it in busyStmt", stmt) - } - if _, ok := stmt.(internalColumnConverter); ok { - t.Errorf("modernc.org/sqlite now offers driver.ColumnConverter on %T, so keep it in busyStmt", stmt) - } -} diff --git a/dbkit/sqlite/busy_test.go b/dbkit/sqlite/busy_test.go index 1343e19..ea3bd86 100644 --- a/dbkit/sqlite/busy_test.go +++ b/dbkit/sqlite/busy_test.go @@ -19,8 +19,6 @@ import ( ) const ( - // busyPoll is how often a call behind a held writer tries the lock again. - busyPoll = 5 * time.Millisecond // busyDeadline is the deadline, and the cancel, of a call behind a held writer. busyDeadline = 100 * time.Millisecond // busyBound is the longest a call behind a held writer may run, a tenth of the busy timeout. @@ -59,6 +57,8 @@ const ( busyNoTable = "VALUES (1), (2)" // busySetTimeout sets a connection's own busy timeout to its one argument in milliseconds. busySetTimeout = "PRAGMA busy_timeout=%d" + // busyShortTimeout is the busy timeout a later step of a query or the close of its rows waits out on purpose. + busyShortTimeout = 300 * time.Millisecond ) // busyShape is one call that waits for the write lock. @@ -67,10 +67,6 @@ type busyShape struct { name string // run makes the call on db under ctx, with prepared holding busyInsert prepared before the writer took the lock. run func(ctx context.Context, db *sql.DB, prepared *sql.Stmt) error - // busy reports that the call's error also matches dbkit.ErrBusy through Classify. - busy bool - // longPoll reports that the call also runs on a handle whose busy poll is the busy timeout. - longPoll bool } // busyShapes are the calls a held writer makes wait. @@ -78,64 +74,34 @@ var busyShapes = []busyShape{ {"an autocommit insert", func(ctx context.Context, db *sql.DB, _ *sql.Stmt) error { _, err := db.ExecContext(ctx, busyInsert) return err - }, false, true}, + }}, {"an insert returning its row", func(ctx context.Context, db *sql.DB, _ *sql.Stmt) error { var a int64 return db.QueryRowContext(ctx, busyInsert+" RETURNING a").Scan(&a) - }, false, true}, + }}, {"BeginTx", func(ctx context.Context, db *sql.DB, _ *sql.Stmt) error { tx, err := db.BeginTx(ctx, nil) if err == nil { _ = tx.Rollback() } return err - }, false, true}, + }}, {"InTx", func(ctx context.Context, db *sql.DB, _ *sql.Stmt) error { return dbkit.InTx(ctx, db, func(tx *sql.Tx) error { _, err := tx.ExecContext(ctx, busyInsert) return err }) - }, false, false}, + }}, {"a statement prepared before the writer", func(ctx context.Context, _ *sql.DB, prepared *sql.Stmt) error { _, err := prepared.ExecContext(ctx) return err - }, false, true}, + }}, } -// busyCase is one shape run on a handle with one busy poll. -type busyCase struct { - // name names the shape and its poll. - name string - // shape is the call. - shape busyShape - // poll is the busy poll of the handle. - poll time.Duration -} - -// busyCases returns every shape at busyPoll, and the long poll shapes again at a busy poll of the busy timeout. -func busyCases() []busyCase { - var cases []busyCase - for _, shape := range busyShapes { - cases = append(cases, busyCase{name: shape.name, shape: shape, poll: busyPoll}) - if shape.longPoll { - cases = append(cases, busyCase{name: shape.name + " at a busy poll of the busy timeout", shape: shape, - poll: optionsBusyTimeout}) - } - } - return cases -} - -// busyOptions returns testOptions with the busy poll given. -func busyOptions(poll time.Duration) sqlite.Options { - opts := testOptions() - opts.BusyPoll = poll - return opts -} - -// busyBehindAWriter returns a handle with poll, busyInsert prepared on it and a writer holding its lock until cleanup. -func busyBehindAWriter(t *testing.T, poll time.Duration) (*sql.DB, *sql.Stmt) { +// busyBehindAWriter returns a handle with busyInsert prepared on it and a writer holding its lock until cleanup. +func busyBehindAWriter(t *testing.T) (*sql.DB, *sql.Stmt) { t.Helper() - db := rulesOpen(t, busyOptions(poll)) + db := rulesOpen(t, testOptions()) rulesExec(t, db, busySchema) busyWarmUp(t, db) prepared, err := db.PrepareContext(t.Context(), busyInsert) @@ -171,14 +137,14 @@ func busyRun(ctx context.Context, shape busyShape, db *sql.DB, prepared *sql.Stm return busyEnd{err: err, waited: time.Since(began)} } -// busyMustEnd fails the test unless the call of shape ended with want within busyBound, busy as shape says. +// busyMustEnd fails the test unless the call of shape ended with want, matching no dbkit.ErrBusy, within busyBound. func busyMustEnd(t *testing.T, shape busyShape, end busyEnd, want error) { t.Helper() if !errors.Is(end.err, want) { t.Errorf("%s error = %v, want one matching %v", shape.name, end.err, want) } - if busy := errors.Is(sqlite.Classify(end.err), dbkit.ErrBusy); busy != shape.busy { - t.Errorf("%s error %v matches dbkit.ErrBusy = %v, want %v", shape.name, end.err, busy, shape.busy) + if errors.Is(sqlite.Classify(end.err), dbkit.ErrBusy) { + t.Errorf("%s error %v matches dbkit.ErrBusy, want no busy match", shape.name, end.err) } if end.waited > busyBound { t.Errorf("%s answered after %v, want within a tenth of the busy timeout %v", shape.name, end.waited, @@ -186,19 +152,19 @@ func busyMustEnd(t *testing.T, shape busyShape, end busyEnd, want error) { } } -// busyMustEndEvery runs every case behind a held writer under the context end makes and expects want. +// busyMustEndEvery runs every shape behind a held writer under the context end makes and expects want. func busyMustEndEvery(t *testing.T, end func(context.Context) (context.Context, context.CancelFunc), want error) { t.Helper() - for _, c := range busyCases() { - t.Run(c.name, func(t *testing.T) { + for _, shape := range busyShapes { + t.Run(shape.name, func(t *testing.T) { t.Parallel() - db, prepared := busyBehindAWriter(t, c.poll) + db, prepared := busyBehindAWriter(t) ctx, cancel := end(t.Context()) defer cancel() - got := busyRun(ctx, c.shape, db, prepared) + got := busyRun(ctx, shape, db, prepared) - busyMustEnd(t, c.shape, got, want) + busyMustEnd(t, shape, got, want) }) } } @@ -309,7 +275,7 @@ func busyRows(t *testing.T, db *sql.DB) int64 { func TestALateLockLeavesACancelledWriteUnwritten(t *testing.T) { t.Parallel() - db := rulesOpen(t, busyOptions(busyPoll)) + db := rulesOpen(t, testOptions()) rulesExec(t, db, busySchema) pinned, other := busyPin(t, db), busyPin(t, db) busyPinExec(t, pinned, busyOtherInsert) @@ -378,7 +344,7 @@ func busyHoldFor(t *testing.T, db *sql.DB, hold time.Duration) <-chan error { func TestAConnectionWritesAgainAfterItsWaitEnded(t *testing.T) { t.Parallel() - db := rulesOpen(t, busyOptions(busyPoll)) + db := rulesOpen(t, testOptions()) rulesExec(t, db, busySchema) prepared := busyPrepareThenEndAWait(t, db, busyPin(t, db)) began := time.Now() @@ -433,7 +399,7 @@ func busyWriteBesideCancelledRows(t *testing.T, db *sql.DB, raw any) (busyEnd, e func TestAPreparedWriteWaitsWhileTheRowsOfACancelledQueryAreOpen(t *testing.T) { t.Parallel() - db := rulesOpen(t, busyOptions(busyPoll)) + db := rulesOpen(t, testOptions()) rulesExec(t, db, busySchema) var got busyEnd @@ -498,83 +464,51 @@ func busyAttachHeld(t *testing.T, db *sql.DB, hold ...string) *sql.Conn { return pinned } -// busyMustStopTheSecondStep reads the first row of rows, runs between, and expects the second step to end at once. -func busyMustStopTheSecondStep(t *testing.T, began time.Time, rows *sql.Rows, between func()) { - t.Helper() - defer func() { _ = rows.Close() }() - - first := rows.Next() - between() - second := rows.Next() +// busyShortOptions returns testOptions with busyShortTimeout as the busy timeout. +func busyShortOptions() sqlite.Options { + opts := testOptions() + opts.BusyTimeout = busyShortTimeout + return opts +} - waited := time.Since(began) - if !first || second { - t.Errorf("Next() = %v, then %v, want the first row and then no second while aux is held", first, second) - } - if err := rows.Err(); err != context.DeadlineExceeded { - t.Errorf("Err() = %v, want exactly the deadline once the second step stopped", err) +// busyMustWaitOut fails the test unless what answered an error that Classify marks busy after busyShortTimeout. +func busyMustWaitOut(t *testing.T, what string, err error, waited time.Duration) { + t.Helper() + if !errors.Is(sqlite.Classify(err), dbkit.ErrBusy) { + t.Errorf("%s error = %v, want busy once the busy timeout passed", what, err) } - if waited > busyBound { - t.Errorf("the second step answered after %v, want within a tenth of the busy timeout %v", waited, - optionsBusyTimeout) + if waited < busyShortTimeout { + t.Errorf("%s answered after %v, want at least the busy timeout %v", what, waited, busyShortTimeout) } } -// busyLaterStepRows returns a connection with aux held exclusive and the rows of busyLaterStep on it under a deadline. -func busyLaterStepRows(t *testing.T) (*sql.Conn, *sql.Rows) { - t.Helper() - pinned := busyAttachHeld(t, rulesOpen(t, busyOptions(busyPoll)), "BEGIN EXCLUSIVE") +func TestALaterStepOfAQueryWaitsOutTheBusyTimeout(t *testing.T) { + t.Parallel() + + pinned := busyAttachHeld(t, rulesOpen(t, busyShortOptions()), "BEGIN EXCLUSIVE") ctx, cancel := context.WithTimeout(t.Context(), busyDeadline) - t.Cleanup(cancel) + defer cancel() rows, err := pinned.QueryContext(ctx, busyLaterStep) if err != nil { t.Fatalf("QueryContext(%q) error = %v, want its first row", busyLaterStep, err) } - return pinned, rows -} - -func TestALaterStepOfAQueryEndsItsWaitWithItsContext(t *testing.T) { - t.Parallel() - - _, rows := busyLaterStepRows(t) - began := time.Now() - - busyMustStopTheSecondStep(t, began, rows, func() {}) -} - -func TestALaterStepAfterAnotherStatementEndsItsWaitWithItsContext(t *testing.T) { - t.Parallel() - - pinned, rows := busyLaterStepRows(t) + defer func() { _ = rows.Close() }() + first := rows.Next() began := time.Now() - busyMustStopTheSecondStep(t, began, rows, func() { busyPinExec(t, pinned, busyNoTable) }) -} - -func TestALaterStepOfAPreparedQueryEndsItsWaitWithItsContext(t *testing.T) { - t.Parallel() + second := rows.Next() - pinned := busyAttachHeld(t, rulesOpen(t, busyOptions(busyPoll)), "BEGIN EXCLUSIVE") - prepared, err := pinned.PrepareContext(t.Context(), busyLaterStep) - if err != nil { - t.Fatalf("PrepareContext(%q) error = %v, want nil", busyLaterStep, err) - } - defer func() { _ = prepared.Close() }() - ctx, cancel := context.WithTimeout(t.Context(), busyDeadline) - defer cancel() - began := time.Now() - rows, err := prepared.QueryContext(ctx) - if err != nil { - t.Fatalf("QueryContext() of %q error = %v, want its first row", busyLaterStep, err) + waited := time.Since(began) + if !first || second { + t.Errorf("Next() = %v, then %v, want the first row and then no second while aux is held", first, second) } - - busyMustStopTheSecondStep(t, began, rows, func() {}) + busyMustWaitOut(t, "the second step", rows.Err(), waited) } -func TestClosingTheRowsOfAWriteEndsItsWaitWithItsContext(t *testing.T) { +func TestClosingTheRowsOfAWriteWaitsOutTheBusyTimeout(t *testing.T) { t.Parallel() - pinned := busyAttachHeld(t, rulesOpen(t, busyOptions(busyPoll)), "BEGIN", busyAuxRead) + pinned := busyAttachHeld(t, rulesOpen(t, busyShortOptions()), "BEGIN", busyAuxRead) ctx, cancel := context.WithTimeout(t.Context(), busyDeadline) defer cancel() began := time.Now() @@ -582,14 +516,7 @@ func TestClosingTheRowsOfAWriteEndsItsWaitWithItsContext(t *testing.T) { err := pinned.QueryRowContext(ctx, busyAuxReturning).Scan(&a) - waited := time.Since(began) - if err != context.DeadlineExceeded { - t.Errorf("Scan() error = %v, want exactly the deadline once the commit at Close stopped", err) - } - if waited > busyBound { - t.Errorf("the commit at Close answered after %v, want within a tenth of the busy timeout %v", waited, - optionsBusyTimeout) - } + busyMustWaitOut(t, "the commit at Close", err, time.Since(began)) } // busyColumn is what database/sql reports of one column. diff --git a/dbkit/sqlite/connector.go b/dbkit/sqlite/connector.go index 2e17ef5..677aee6 100644 --- a/dbkit/sqlite/connector.go +++ b/dbkit/sqlite/connector.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log/slog" + "strconv" "time" modernc "modernc.org/sqlite" @@ -37,12 +38,12 @@ type connector struct { name string // path is the absolute path of the file. path string - // busyTimeout is how long a busy open or a connection waiting for a lock keeps trying. + // busyTimeout is how long a busy open keeps trying. busyTimeout time.Duration - // busyPoll is how often a busy open or a connection waiting for a lock tries again. + // busyPoll is how often a busy open tries again and the longest one try blocks. busyPoll time.Duration - // wait makes the waits of a new connection end with their context. - wait func(conn driver.Conn) (driver.Conn, error) + // restore sets a connection's busy timeout back to the option. + restore string // logger receives the skipped optimize notes. logger *slog.Logger } @@ -53,7 +54,7 @@ func newConnector(drv *modernc.Driver, path string, opts Options) *connector { if logger == nil { logger = slog.New(slog.DiscardHandler) } - c := &connector{ + return &connector{ driver: drv, open: drv.Open, opening: make(chan struct{}, 1), @@ -61,13 +62,12 @@ func newConnector(drv *modernc.Driver, path string, opts Options) *connector { path: path, busyTimeout: opts.BusyTimeout, busyPoll: opts.BusyPoll, + restore: "PRAGMA busy_timeout=" + strconv.FormatInt(opts.BusyTimeout.Milliseconds(), 10), logger: logger, } - c.wait = c.waitOn - return c } -// Connect opens one connection, waiting while busy, optimizes it, ends its waits with ctx and passes it to the seam. +// Connect opens one connection, waiting while busy, optimizes it and passes it to the seam. func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err := ctx.Err(); err != nil { return nil, err @@ -79,11 +79,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err := c.optimize(ctx, conn.(driver.ExecerContext)); err != nil { return nil, errors.Join(err, conn.Close()) } - waiting, err := c.wait(conn) - if err != nil { - return nil, errors.Join(err, conn.Close()) - } - return seam.Wrap(c.path, waiting), nil + return seam.Wrap(c.path, conn), nil } // openWaiting opens one connection, trying again every busy poll while busy, until the busy timeout passes or ctx ends. @@ -181,17 +177,28 @@ func gaveUp(ctx context.Context, busy error) error { return fmt.Errorf("dbkit: open the database: %w", ctx.Err()) } +// ended returns err marked with the error of ctx once ctx has ended, and err as it is before. +func ended(ctx context.Context, err error) error { + if end := ctx.Err(); end != nil { + return fmt.Errorf("%w: %w", err, end) + } + return err +} + // Driver returns the private driver value of the handle. func (c *connector) Driver() driver.Driver { return c.driver } -// optimize lowers the busy timeout of conn, runs PRAGMA optimize and skips a busy answer with a note. +// optimize runs PRAGMA optimize at a zero busy timeout, skips a busy answer with a note and restores BusyTimeout. func (c *connector) optimize(ctx context.Context, conn driver.ExecerContext) error { _, err := conn.ExecContext(ctx, lowerBusyTimeout, nil) if err == nil { _, err = conn.ExecContext(ctx, optimizeStatement, nil) } + if _, restoreErr := conn.ExecContext(context.WithoutCancel(ctx), c.restore, nil); restoreErr != nil { + return errors.Join(err, restoreErr) + } if errors.Is(Classify(err), dbkit.ErrBusy) { c.logger.InfoContext(ctx, optimizeSkipped, slog.String("path", c.path), slog.Any("error", err)) return nil diff --git a/dbkit/sqlite/connector_internal_test.go b/dbkit/sqlite/connector_internal_test.go index ec5bb77..f8caff4 100644 --- a/dbkit/sqlite/connector_internal_test.go +++ b/dbkit/sqlite/connector_internal_test.go @@ -26,6 +26,8 @@ const ( internalLower = "PRAGMA busy_timeout=0" // internalOptimize is the optimize statement every new connection runs. internalOptimize = "PRAGMA optimize=0x10002" + // internalRestore is the statement that sets internalOptions' busy timeout back. + internalRestore = "PRAGMA busy_timeout=1500" // internalBusyPoll is the busy poll of internalOptions, the smallest Open accepts. internalBusyPoll = time.Millisecond // internalLongBusyTimeout is a busy timeout no internal test waits out. @@ -57,6 +59,8 @@ var ( errStepFailed = errors.New("connector test: the statement failed on purpose") // errCloseFailed is the error the fake connection's Close answers with. errCloseFailed = errors.New("connector test: the close failed on purpose") + // errRestoreFailed is the error the fake connection's restore answers with. + errRestoreFailed = errors.New("connector test: the restore failed on purpose") // errEndCause is the cause a test ends a context with. errEndCause = errors.New("connector test: the context ended with this cause on purpose") ) @@ -73,8 +77,12 @@ type internalConn struct { fail map[string]error // closeErr is the error Close answers with. closeErr error + // after runs after each statement with its text. + after func(query string) // ran lists every statement in the order it ran. ran []string + // ended lists every statement that ran on an ended context. + ended []string // closed reports that Close ran. closed bool } @@ -96,19 +104,20 @@ func (c *internalConn) Begin() (driver.Tx, error) { } // ExecContext records query and answers the error chosen for it. -func (c *internalConn) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) { +func (c *internalConn) ExecContext(ctx context.Context, query string, _ []driver.NamedValue) (driver.Result, error) { c.ran = append(c.ran, query) + if ctx.Err() != nil { + c.ended = append(c.ended, query) + } + if c.after != nil { + c.after(query) + } if err := c.fail[query]; err != nil { return nil, err } return driver.RowsAffected(0), nil } -// internalNoWait returns conn as it is, in place of the busy wait a connection the driver opened gets. -func internalNoWait(conn driver.Conn) (driver.Conn, error) { - return conn, nil -} - // internalConnector returns a connector that opens conn, or fails with openErr, and counts its opens. func internalConnector(conn driver.Conn, openErr error) (*connector, *int) { c := newConnector(&modernc.Driver{}, "/srv/site/site.db", internalOptions()) @@ -117,7 +126,6 @@ func internalConnector(conn driver.Conn, openErr error) (*connector, *int) { opened++ return conn, openErr } - c.wait = internalNoWait return c, &opened } @@ -163,7 +171,6 @@ func internalWaiting(timeout, poll time.Duration, open func(name string) (driver opts.BusyTimeout, opts.BusyPoll = timeout, poll c := newConnector(&modernc.Driver{}, "/srv/site/site.db", opts) c.open = open - c.wait = internalNoWait return c } @@ -335,6 +342,25 @@ func internalMustOpenAgain(t *testing.T, c *connector, held *internalHeld) { } } +func TestEndedMarksAnErrorOnlyOnceItsContextEnded(t *testing.T) { + t.Parallel() + + cancelled, cancel := context.WithCancel(t.Context()) + cancel() + + if got := ended(t.Context(), errStepFailed); got != errStepFailed { + t.Errorf("ended() under a live context = %v, want the error as it is", got) + } + got := ended(cancelled, errStepFailed) + if !errors.Is(got, errStepFailed) || !errors.Is(got, context.Canceled) { + t.Errorf("ended() under a cancelled context = %v, want an error matching both the error and context.Canceled", + got) + } + if want := errStepFailed.Error() + ": " + context.Canceled.Error(); got == nil || got.Error() != want { + t.Errorf("ended() under a cancelled context = %v, want %q", got, want) + } +} + func TestConnectRefusesAnEndedContext(t *testing.T) { t.Parallel() @@ -487,11 +513,6 @@ func TestTheOptimizeStepRunsInOrder(t *testing.T) { fake := &internalConn{} c, _ := internalConnector(fake, nil) - var before [][]string - c.wait = func(conn driver.Conn) (driver.Conn, error) { - before = append(before, slices.Clone(fake.ran)) - return conn, nil - } conn, err := c.Connect(t.Context()) @@ -501,15 +522,7 @@ func TestTheOptimizeStepRunsInOrder(t *testing.T) { if fake.closed { t.Errorf("closed = %v, want the open connection left open", fake.closed) } - internalMustRun(t, fake, internalLower, internalOptimize) - if want := []string{internalLower, internalOptimize}; len(before) != 1 || !slices.Equal(before[0], want) { - t.Errorf("statements before each busy wait = %q, want one busy wait after %q", before, want) - } -} - -// internalWaitFails answers errStepFailed in place of the busy wait of a connection. -func internalWaitFails(driver.Conn) (driver.Conn, error) { - return nil, errStepFailed + internalMustRun(t, fake, internalLower, internalOptimize, internalRestore) } func TestAFailedStepClosesTheConnection(t *testing.T) { @@ -518,23 +531,20 @@ func TestAFailedStepClosesTheConnection(t *testing.T) { cases := []struct { // name names the step that fails. name string - // failed is the statement that fails, empty when no statement does. + // failed is the statement that fails. failed string - // wait stands in for the busy wait of the connection. - wait func(driver.Conn) (driver.Conn, error) // ran lists the statements the connection runs. ran []string }{ - {"the lowering", internalLower, internalNoWait, []string{internalLower}}, - {"the optimize", internalOptimize, internalNoWait, []string{internalLower, internalOptimize}}, - {"the busy wait", "", internalWaitFails, []string{internalLower, internalOptimize}}, + {"the lowering", internalLower, []string{internalLower, internalRestore}}, + {"the optimize", internalOptimize, []string{internalLower, internalOptimize, internalRestore}}, + {"the restore", internalRestore, []string{internalLower, internalOptimize, internalRestore}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() fake := &internalConn{fail: map[string]error{c.failed: errStepFailed}} connector, _ := internalConnector(fake, nil) - connector.wait = c.wait conn, err := connector.Connect(t.Context()) @@ -562,6 +572,40 @@ func TestAFailedCloseJoinsTheStepError(t *testing.T) { } } +func TestAFailedRestoreKeepsTheStepError(t *testing.T) { + t.Parallel() + + fake := &internalConn{fail: map[string]error{internalOptimize: errStepFailed, internalRestore: errRestoreFailed}} + c, _ := internalConnector(fake, nil) + + _, err := c.Connect(t.Context()) + + if !errors.Is(err, errStepFailed) || !errors.Is(err, errRestoreFailed) { + t.Errorf("Connect() error = %v, want both the step's error and the restore error", err) + } +} + +func TestTheRestoreOutlivesAnEndedContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + fake := &internalConn{after: func(query string) { + if query == internalOptimize { + cancel() + } + }} + c, _ := internalConnector(fake, nil) + + if _, err := c.Connect(ctx); err != nil { + t.Fatalf("Connect() error = %v, want nil", err) + } + + internalMustRun(t, fake, internalLower, internalOptimize, internalRestore) + if len(fake.ended) != 0 { + t.Errorf("statements on an ended context = %q, want the restore run on a live one", fake.ended) + } +} + func TestConnectAnswersAnErrorThatOnlyReadsBusy(t *testing.T) { t.Parallel() diff --git a/dbkit/sqlite/connector_test.go b/dbkit/sqlite/connector_test.go index dc2935d..81fff1e 100644 --- a/dbkit/sqlite/connector_test.go +++ b/dbkit/sqlite/connector_test.go @@ -121,12 +121,13 @@ func TestANewConnectionOpensWhileAWriterHoldsTheLock(t *testing.T) { connectorMustSkip(t, records, path) } -// connectorMustWaitWithItsContext fails the test unless conn reads busy_timeout 0 and its wait ends at its deadline. +// connectorMustWaitWithItsContext fails the test unless conn reads BusyTimeout in ms and its wait ends at its deadline. func connectorMustWaitWithItsContext(t *testing.T, conn *sql.Conn) { t.Helper() var busy int64 - if err := conn.QueryRowContext(t.Context(), "PRAGMA busy_timeout").Scan(&busy); err != nil || busy != 0 { - t.Errorf("PRAGMA busy_timeout = %d, %v, want 0 with dbkit's busy handler in place of SQLite's", busy, err) + err := conn.QueryRowContext(t.Context(), "PRAGMA busy_timeout").Scan(&busy) + if want := optionsBusyTimeout.Milliseconds(); err != nil || busy != want { + t.Errorf("PRAGMA busy_timeout = %d, %v, want %d, the busy timeout in ms", busy, err, want) } ctx, cancel := context.WithTimeout(t.Context(), connectorWaitDeadline) defer cancel() @@ -368,6 +369,31 @@ func TestTheFirstOpenEndsWithItsContextAtTheLongestBusyPoll(t *testing.T) { connectorMustEndAtItsDeadline(t, db, holder, false) } +// connectorSerializer is the Serialize method of the driver's own connection. +type connectorSerializer interface { + // Serialize returns the main database as bytes. + Serialize() ([]byte, error) +} + +func TestRawHandsOutTheDriversConnection(t *testing.T) { + t.Parallel() + + conn := busyPin(t, rulesOpen(t, testOptions())) + var got any + + err := conn.Raw(func(raw any) error { + got = raw + return nil + }) + + if err != nil { + t.Fatalf("Raw() error = %v, want nil", err) + } + if _, ok := got.(connectorSerializer); !ok { + t.Errorf("Raw() hands out %T, want the driver's own connection with Serialize() ([]byte, error)", got) + } +} + // connectorWrapped is a connection the test's wrapper returned. type connectorWrapped struct { driver.Conn diff --git a/dbkit/sqlite/go.mod b/dbkit/sqlite/go.mod index de23f14..eb481c9 100644 --- a/dbkit/sqlite/go.mod +++ b/dbkit/sqlite/go.mod @@ -6,7 +6,6 @@ require ( github.com/gopherium/framework/dbkit v0.3.0 github.com/pressly/goose/v3 v3.27.2 golang.org/x/sys v0.49.0 - modernc.org/libc v1.79.0 modernc.org/sqlite v1.61.0 ) @@ -20,6 +19,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.24.0 // indirect + modernc.org/libc v1.79.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect ) diff --git a/dbkit/sqlite/options.go b/dbkit/sqlite/options.go index 25f75cd..e61856c 100644 --- a/dbkit/sqlite/options.go +++ b/dbkit/sqlite/options.go @@ -38,7 +38,7 @@ func (s Synchronous) pragma() string { type Options struct { // BusyTimeout is how long a connection waits for a lock, from 1ms to 596h31m23.647s. BusyTimeout time.Duration - // BusyPoll is how often a lock wait tries again and the longest an opening try blocks, from 1ms up to BusyTimeout. + // BusyPoll is how often a busy open tries again and the longest one open try blocks, from 1ms up to BusyTimeout. BusyPoll time.Duration // CacheSize is the page cache of each connection in kibibytes, from 1 to 2147483647. CacheSize int diff --git a/dbkit/sqlite/rules_test.go b/dbkit/sqlite/rules_test.go index 9f68e7e..c5fbf02 100644 --- a/dbkit/sqlite/rules_test.go +++ b/dbkit/sqlite/rules_test.go @@ -125,9 +125,10 @@ func TestEveryConnectionWaitsTheBusyTimeout(t *testing.T) { for i, conn := range rulesConns(t, db) { var timeout int64 - if err := conn.QueryRowContext(t.Context(), "PRAGMA busy_timeout").Scan(&timeout); err != nil || timeout != 0 { - t.Errorf("PRAGMA busy_timeout on connection %d = %d, %v, want 0 with dbkit's busy handler in place of SQLite's", - i, timeout, err) + err := conn.QueryRowContext(t.Context(), "PRAGMA busy_timeout").Scan(&timeout) + if want := rulesShortBusyTimeout.Milliseconds(); err != nil || timeout != want { + t.Errorf("PRAGMA busy_timeout on connection %d = %d, %v, want %d, the busy timeout in ms", i, timeout, err, + want) } waited, err := rulesBegin(t.Context(), conn) diff --git a/dbkit/sqlite/share_test.go b/dbkit/sqlite/share_test.go index 918e70d..4b5b26d 100644 --- a/dbkit/sqlite/share_test.go +++ b/dbkit/sqlite/share_test.go @@ -262,7 +262,7 @@ func shareMustReadTheRules(t *testing.T, i int, conn *sql.Conn) { t.Helper() pragmas := map[string]int64{ "PRAGMA foreign_keys": 1, - "PRAGMA busy_timeout": 0, + "PRAGMA busy_timeout": optionsBusyTimeout.Milliseconds(), "PRAGMA temp_store": 2, "PRAGMA cache_size": -optionsCacheSize, "PRAGMA synchronous": 1, diff --git a/dbkit/sqlite/sqlitetest/faults_test.go b/dbkit/sqlite/sqlitetest/faults_test.go index e7f5f50..ed110b2 100644 --- a/dbkit/sqlite/sqlitetest/faults_test.go +++ b/dbkit/sqlite/sqlitetest/faults_test.go @@ -535,13 +535,13 @@ func mustWaitBehind(t *testing.T, conns []*sql.Conn, i int) { } } -// mustKeepTheRules fails the test unless connection i of conns, from testOptions, carries every rule. +// mustKeepTheRules fails the test unless connection i, from testOptions at testShortBusyTimeout, keeps every rule. func mustKeepTheRules(t *testing.T, conns []*sql.Conn, i int) { t.Helper() conn := conns[i] pragmas := map[string]int64{ "PRAGMA foreign_keys": 1, - "PRAGMA busy_timeout": 0, + "PRAGMA busy_timeout": testShortBusyTimeout.Milliseconds(), "PRAGMA temp_store": 2, "PRAGMA cache_size": -testCacheSize, "PRAGMA synchronous": 1, diff --git a/dbkit/sqlite/sqlitetest/template_test.go b/dbkit/sqlite/sqlitetest/template_test.go index fdd62bd..3b27d4b 100644 --- a/dbkit/sqlite/sqlitetest/template_test.go +++ b/dbkit/sqlite/sqlitetest/template_test.go @@ -278,7 +278,7 @@ func templateMustReadTheRules(t *testing.T, i int, conn *sql.Conn, opts sqlite.O t.Helper() pragmas := map[string]int64{ "PRAGMA foreign_keys": 1, - "PRAGMA busy_timeout": 0, + "PRAGMA busy_timeout": opts.BusyTimeout.Milliseconds(), "PRAGMA temp_store": 2, "PRAGMA cache_size": -int64(opts.CacheSize), "PRAGMA synchronous": templateSynchronous[opts.Synchronous], From 5f4155faf87afc2d8acd866b38c8aac75ea6b4e4 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Thu, 22 Oct 2026 03:01:52 +0200 Subject: [PATCH 3/3] test(dbkit/sqlite): start the later-step deadline right before the step under test --- dbkit/sqlite/busy_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dbkit/sqlite/busy_test.go b/dbkit/sqlite/busy_test.go index ea3bd86..a7eff0f 100644 --- a/dbkit/sqlite/busy_test.go +++ b/dbkit/sqlite/busy_test.go @@ -486,7 +486,7 @@ func TestALaterStepOfAQueryWaitsOutTheBusyTimeout(t *testing.T) { t.Parallel() pinned := busyAttachHeld(t, rulesOpen(t, busyShortOptions()), "BEGIN EXCLUSIVE") - ctx, cancel := context.WithTimeout(t.Context(), busyDeadline) + ctx, cancel := context.WithCancel(t.Context()) defer cancel() rows, err := pinned.QueryContext(ctx, busyLaterStep) if err != nil { @@ -495,6 +495,7 @@ func TestALaterStepOfAQueryWaitsOutTheBusyTimeout(t *testing.T) { defer func() { _ = rows.Close() }() first := rows.Next() began := time.Now() + defer time.AfterFunc(busyDeadline, cancel).Stop() second := rows.Next() @@ -509,12 +510,19 @@ func TestClosingTheRowsOfAWriteWaitsOutTheBusyTimeout(t *testing.T) { t.Parallel() pinned := busyAttachHeld(t, rulesOpen(t, busyShortOptions()), "BEGIN", busyAuxRead) - ctx, cancel := context.WithTimeout(t.Context(), busyDeadline) + ctx, cancel := context.WithCancel(t.Context()) defer cancel() + rows, err := pinned.QueryContext(ctx, busyAuxReturning) + if err != nil { + t.Fatalf("QueryContext(%q) error = %v, want its row", busyAuxReturning, err) + } + if !rows.Next() { + t.Fatalf("Next() = false, want the row of %q, error %v", busyAuxReturning, rows.Err()) + } began := time.Now() - var a int64 + defer time.AfterFunc(busyDeadline, cancel).Stop() - err := pinned.QueryRowContext(ctx, busyAuxReturning).Scan(&a) + err = rows.Close() busyMustWaitOut(t, "the commit at Close", err, time.Since(began)) }