Skip to content

Commit abe1e89

Browse files
authored
chore: increase parallelism of TestWorkspaceQuota (#6710)
This does a lot of build operations, so having multiple provisioner daemons is great. We were actually approaching the ceiling here for test duration!
1 parent 5cbe360 commit abe1e89

File tree

4 files changed

+90
-9
lines changed

4 files changed

+90
-9
lines changed

coderd/database/db.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,42 @@ func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) {
6767
return time.Since(start), err
6868
}
6969

70-
// InTx performs database operations inside a transaction.
7170
func (q *sqlQuerier) InTx(function func(Store) error, txOpts *sql.TxOptions) error {
71+
_, inTx := q.db.(*sqlx.Tx)
72+
isolation := sql.LevelDefault
73+
if txOpts != nil {
74+
isolation = txOpts.Isolation
75+
}
76+
77+
// If we are not already in a transaction, and we are running in serializable
78+
// mode, we need to run the transaction in a retry loop. The caller should be
79+
// prepared to allow retries if using serializable mode.
80+
// If we are in a transaction already, the parent InTx call will handle the retry.
81+
// We do not want to duplicate those retries.
82+
if !inTx && isolation == sql.LevelSerializable {
83+
// This is an arbitrarily chosen number.
84+
const retryAmount = 3
85+
var err error
86+
attempts := 0
87+
for attempts = 0; attempts < retryAmount; attempts++ {
88+
err = q.runTx(function, txOpts)
89+
if err == nil {
90+
// Transaction succeeded.
91+
return nil
92+
}
93+
if err != nil && !IsSerializedError(err) {
94+
// We should only retry if the error is a serialization error.
95+
return err
96+
}
97+
}
98+
// Transaction kept failing in serializable mode.
99+
return xerrors.Errorf("transaction failed after %d attempts: %w", attempts, err)
100+
}
101+
return q.runTx(function, txOpts)
102+
}
103+
104+
// InTx performs database operations inside a transaction.
105+
func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) error {
72106
if _, ok := q.db.(*sqlx.Tx); ok {
73107
// If the current inner "db" is already a transaction, we just reuse it.
74108
// We do not need to handle commit/rollback as the outer tx will handle

coderd/database/db_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,44 @@ import (
88
"testing"
99

1010
"github.com/google/uuid"
11+
"github.com/lib/pq"
1112
"github.com/stretchr/testify/require"
1213

1314
"github.com/coder/coder/coderd/database"
1415
"github.com/coder/coder/coderd/database/migrations"
1516
"github.com/coder/coder/coderd/database/postgres"
1617
)
1718

19+
func TestSerializedRetry(t *testing.T) {
20+
t.Parallel()
21+
if testing.Short() {
22+
t.SkipNow()
23+
}
24+
25+
sqlDB := testSQLDB(t)
26+
db := database.New(sqlDB)
27+
28+
called := 0
29+
txOpts := &sql.TxOptions{Isolation: sql.LevelSerializable}
30+
err := db.InTx(func(tx database.Store) error {
31+
// Test nested error
32+
return tx.InTx(func(tx database.Store) error {
33+
// The easiest way to mock a serialization failure is to
34+
// return a serialization failure error.
35+
called++
36+
return &pq.Error{
37+
Code: "40001",
38+
Message: "serialization_failure",
39+
}
40+
}, txOpts)
41+
}, txOpts)
42+
require.Error(t, err, "should fail")
43+
// The double "execute transaction: execute transaction" is from the nested transactions.
44+
// Just want to make sure we don't try 9 times.
45+
require.Equal(t, err.Error(), "transaction failed after 3 attempts: execute transaction: execute transaction: pq: serialization_failure", "error message")
46+
require.Equal(t, called, 3, "should retry 3 times")
47+
}
48+
1849
func TestNestedInTx(t *testing.T) {
1950
t.Parallel()
2051
if testing.Short() {

coderd/database/errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import (
66
"github.com/lib/pq"
77
)
88

9+
func IsSerializedError(err error) bool {
10+
var pqErr *pq.Error
11+
if errors.As(err, &pqErr) {
12+
return pqErr.Code.Name() == "serialization_failure"
13+
}
14+
return false
15+
}
16+
917
// IsUniqueViolation checks if the error is due to a unique violation.
1018
// If one or more specific unique constraints are given as arguments,
1119
// the error must be caused by one of them. If no constraints are given,

enterprise/coderd/workspacequota_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package coderd_test
22

33
import (
44
"context"
5+
"sync"
56
"testing"
67

78
"github.com/google/uuid"
9+
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
911

1012
"github.com/coder/coder/coderd/coderdtest"
@@ -37,12 +39,12 @@ func TestWorkspaceQuota(t *testing.T) {
3739
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
3840
defer cancel()
3941
max := 1
40-
client := coderdenttest.New(t, &coderdenttest.Options{
42+
client, _, api := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
4143
UserWorkspaceQuota: max,
42-
Options: &coderdtest.Options{
43-
IncludeProvisionerDaemon: true,
44-
},
4544
})
45+
coderdtest.NewProvisionerDaemon(t, api.AGPL)
46+
coderdtest.NewProvisionerDaemon(t, api.AGPL)
47+
coderdtest.NewProvisionerDaemon(t, api.AGPL)
4648

4749
user := coderdtest.CreateFirstUser(t, client)
4850
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
@@ -104,12 +106,18 @@ func TestWorkspaceQuota(t *testing.T) {
104106
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
105107

106108
// Spin up three workspaces fine
109+
var wg sync.WaitGroup
107110
for i := 0; i < 3; i++ {
108-
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
109-
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
110-
verifyQuota(ctx, t, client, i+1, 3)
111-
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
111+
wg.Add(1)
112+
go func() {
113+
defer wg.Done()
114+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
115+
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
116+
assert.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
117+
}()
112118
}
119+
wg.Wait()
120+
verifyQuota(ctx, t, client, 3, 3)
113121

114122
// Next one must fail
115123
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)

0 commit comments

Comments
 (0)