diff options
author | Andrii Zavorotnii <andrii.zavorotnii@gmail.com> | 2020-08-28 08:43:21 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-29 00:43:21 +0900 |
commit | 862b95943f99f3b40e317a79d41c27ac4b742011 (patch) | |
tree | 8ddf5deb99c4466eed725e130060083b29558e55 /sqlite3.go | |
parent | Use go-pointer instead of uintptr hacks. (#814) (diff) | |
download | golite-862b95943f99f3b40e317a79d41c27ac4b742011.tar.gz golite-862b95943f99f3b40e317a79d41c27ac4b742011.tar.xz |
Fix "cannot start a transaction within a transaction" issue (#764) (#765)
* Fix "cannot start a transaction within a transaction" issue
[why]
If db.BeginTx(ctx, nil) context is cancelled too fast, "BEGIN" statement can be
completed inside DB, but we still try to cancel it with sqlite3_interrupt.
In such case we get context.Cancelled or context.DeadlineExceeded from exec(),
but operation really completed. Connection returned into pool, and returns "cannot
start a transaction within a transaction" error for next db.BeginTx() call.
[how]
Handle status code returned from cancelled operation.
[testing]
Added unit-test which reproduces issue.
* Reduce TestQueryRowContextCancelParallel concurrency
[why]
Tests times out in travis-ci when run with -race option.
Diffstat (limited to 'sqlite3.go')
-rw-r--r-- | sqlite3.go | 21 |
1 files changed, 16 insertions, 5 deletions
@@ -1918,6 +1918,14 @@ func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) { return s.exec(context.Background(), list) } +func isInterruptErr(err error) bool { + sqliteErr, ok := err.(Error) + if ok { + return sqliteErr.Code == ErrInterrupt + } + return false +} + // exec executes a query that doesn't return rows. Attempts to honor context timeout. func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result, error) { if ctx.Done() == nil { @@ -1933,19 +1941,22 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result r, err := s.execSync(args) resultCh <- result{r, err} }() + var rv result select { - case rv := <-resultCh: - return rv.r, rv.err + case rv = <-resultCh: case <-ctx.Done(): select { - case <-resultCh: // no need to interrupt + case rv = <-resultCh: // no need to interrupt, operation completed in db default: // this is still racy and can be no-op if executed between sqlite3_* calls in execSync. C.sqlite3_interrupt(s.c.db) - <-resultCh // ensure goroutine completed + rv = <-resultCh // wait for goroutine completed + if isInterruptErr(rv.err) { + return nil, ctx.Err() + } } - return nil, ctx.Err() } + return rv.r, rv.err } func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) { |