mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
chore: fix concurrent CommitQuota
transactions for unrelated users/orgs (#15261)
The failure condition being fixed is `w1` and `w2` could belong to different users, organizations, and templates and still cause a serializable failure if run concurrently. This is because the old query did a `seq scan` on the `workspace_builds` table. Since that is the table being updated, we really want to prevent that. So before this would fail for any 2 workspaces. Now it only fails if `w1` and `w2` are owned by the same user and organization.
This commit is contained in:
@ -135,7 +135,8 @@ func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) {
|
||||
if o.dumpOnFailure {
|
||||
t.Cleanup(func() { DumpOnFailure(t, connectionURL) })
|
||||
}
|
||||
db = database.New(sqlDB)
|
||||
// Unit tests should not retry serial transaction failures.
|
||||
db = database.New(sqlDB, database.WithSerialRetryCount(1))
|
||||
|
||||
ps, err = pubsub.New(context.Background(), o.logger, sqlDB, connectionURL)
|
||||
require.NoError(t, err)
|
||||
|
73
coderd/database/dbtestutil/tx.go
Normal file
73
coderd/database/dbtestutil/tx.go
Normal file
@ -0,0 +1,73 @@
|
||||
package dbtestutil
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
type DBTx struct {
|
||||
database.Store
|
||||
mu sync.Mutex
|
||||
done chan error
|
||||
finalErr chan error
|
||||
}
|
||||
|
||||
// StartTx starts a transaction and returns a DBTx object. This allows running
|
||||
// 2 transactions concurrently in a test more easily.
|
||||
// Example:
|
||||
//
|
||||
// a := StartTx(t, db, opts)
|
||||
// b := StartTx(t, db, opts)
|
||||
//
|
||||
// a.GetUsers(...)
|
||||
// b.GetUsers(...)
|
||||
//
|
||||
// require.NoError(t, a.Done()
|
||||
func StartTx(t *testing.T, db database.Store, opts *database.TxOptions) *DBTx {
|
||||
done := make(chan error)
|
||||
finalErr := make(chan error)
|
||||
txC := make(chan database.Store)
|
||||
|
||||
go func() {
|
||||
t.Helper()
|
||||
once := sync.Once{}
|
||||
count := 0
|
||||
|
||||
err := db.InTx(func(store database.Store) error {
|
||||
// InTx can be retried
|
||||
once.Do(func() {
|
||||
txC <- store
|
||||
})
|
||||
count++
|
||||
if count > 1 {
|
||||
// If you recursively call InTx, then don't use this.
|
||||
t.Logf("InTx called more than once: %d", count)
|
||||
assert.NoError(t, xerrors.New("InTx called more than once, this is not allowed with the StartTx helper"))
|
||||
}
|
||||
|
||||
<-done
|
||||
// Just return nil. The caller should be checking their own errors.
|
||||
return nil
|
||||
}, opts)
|
||||
finalErr <- err
|
||||
}()
|
||||
|
||||
txStore := <-txC
|
||||
close(txC)
|
||||
|
||||
return &DBTx{Store: txStore, done: done, finalErr: finalErr}
|
||||
}
|
||||
|
||||
// Done can only be called once. If you call it twice, it will panic.
|
||||
func (tx *DBTx) Done() error {
|
||||
tx.mu.Lock()
|
||||
defer tx.mu.Unlock()
|
||||
|
||||
close(tx.done)
|
||||
return <-tx.finalErr
|
||||
}
|
Reference in New Issue
Block a user