Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion dbkit/sqlite/busy.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,35 @@ func (c *busyConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.T
if err != nil {
return nil, ended(ctx, err)
}
return tx, nil
return &busyTx{Tx: tx, wait: c.wait, ctx: ctx}, nil
}

// busyTx is a transaction whose commit waits under the context BeginTx was given.
//
// database/sql's Tx.Commit takes no context, so without this wrapper a commit
// that waits for a lock would run with no span and wait up to BusyTimeout. The
// rollback stays the driver's: database/sql rolls back on an ended context, and
// a rollback that stopped early would return a connection still inside a
// transaction to the pool.
type busyTx struct {
// Tx is the driver's transaction.
driver.Tx
// wait is the connection's wait.
wait *busyWait
// ctx is the context BeginTx was given.
ctx context.Context
}

// Commit commits the transaction, and a wait for a lock ends with the transaction's context.
func (t *busyTx) Commit() error {
defer t.wait.during(t.ctx)()
err := t.Tx.Commit()
if err == nil {
// The commit took effect. A context that ended meanwhile must not turn
// it into an error the caller would retry.
return nil
}
return ended(t.ctx, err)
}

// ExecContext runs a statement, and answers the error of ctx alone when it failed after ctx ended.
Expand Down
28 changes: 28 additions & 0 deletions dbkit/sqlite/busy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,34 @@ func TestClosingTheRowsOfAWriteEndsItsWaitWithItsContext(t *testing.T) {
}
}

func TestACommitEndsItsWaitWithTheContextOfBeginTx(t *testing.T) {
t.Parallel()

pinned := busyAttachHeld(t, rulesOpen(t, busyOptions(busyPoll)), "BEGIN", busyAuxRead)
ctx, cancel := context.WithTimeout(t.Context(), busyDeadline)
defer cancel()

tx, err := pinned.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("BeginTx() error = %v, want nil", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, "INSERT INTO aux.aux_rows VALUES (1)"); err != nil {
t.Fatalf("ExecContext() error = %v, want nil", err)
}
began := time.Now()

err = tx.Commit()

waited := time.Since(began)
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("Commit() error = %v, want one matching context.DeadlineExceeded", err)
}
if waited > busyBound {
t.Errorf("Commit() answered after %v, want within a tenth of the busy timeout %v", waited, optionsBusyTimeout)
}
}

// busyColumn is what database/sql reports of one column.
type busyColumn struct {
// name is the column's name.
Expand Down
Loading