Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f4a2d28

Browse files
spikecurtiskylecarbs
authored andcommitted
Spike/222 workspace build order (#1534)
* chore: refactor before_id/after_id to build_number Signed-off-by: Spike Curtis <[email protected]> * pagination of workspace_builds Signed-off-by: Spike Curtis <[email protected]> * Disable parallel on postgres tests Signed-off-by: Spike Curtis <[email protected]> * Fix lint Signed-off-by: Spike Curtis <[email protected]> * Fix workspace build postgres query Signed-off-by: Spike Curtis <[email protected]> * Fix JS tests Signed-off-by: Spike Curtis <[email protected]> * Fix workspace builds postgres query Signed-off-by: Spike Curtis <[email protected]>
1 parent 668e448 commit f4a2d28

24 files changed

+397
-226
lines changed

cli/resetpassword_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import (
1515
"github.com/coder/coder/pty/ptytest"
1616
)
1717

18+
// nolint:paralleltest
1819
func TestResetPassword(t *testing.T) {
19-
t.Parallel()
20+
// postgres.Open() seems to be creating race conditions when run in parallel.
21+
// t.Parallel()
2022

2123
if runtime.GOOS != "linux" || testing.Short() {
2224
// Skip on non-Linux because it spawns a PostgreSQL instance.

cli/server_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ import (
3232
)
3333

3434
// This cannot be ran in parallel because it uses a signal.
35-
// nolint:tparallel
35+
// nolint:paralleltest
3636
func TestServer(t *testing.T) {
3737
t.Run("Production", func(t *testing.T) {
38-
t.Parallel()
38+
// postgres.Open() seems to be creating race conditions when run in parallel.
39+
// t.Parallel()
3940
if runtime.GOOS != "linux" || testing.Short() {
4041
// Skip on non-Linux because it spawns a PostgreSQL instance.
4142
t.SkipNow()

coderd/autobuild/executor/lifecycle_executor.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (e *Executor) runOnce(t time.Time) error {
5757

5858
for _, ws := range eligibleWorkspaces {
5959
// Determine the workspace state based on its latest build.
60-
priorHistory, err := db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(e.ctx, ws.ID)
60+
priorHistory, err := db.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
6161
if err != nil {
6262
e.log.Warn(e.ctx, "get latest workspace build",
6363
slog.F("workspace_id", ws.ID),
@@ -152,12 +152,8 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
152152
return xerrors.Errorf("get workspace template: %w", err)
153153
}
154154

155-
priorHistoryID := uuid.NullUUID{
156-
UUID: priorHistory.ID,
157-
Valid: true,
158-
}
155+
priorBuildNumber := priorHistory.BuildNumber
159156

160-
var newWorkspaceBuild database.WorkspaceBuild
161157
// This must happen in a transaction to ensure history can be inserted, and
162158
// the prior history can update it's "after" column to point at the new.
163159
workspaceBuildID := uuid.New()
@@ -186,13 +182,13 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
186182
if err != nil {
187183
return xerrors.Errorf("insert provisioner job: %w", err)
188184
}
189-
newWorkspaceBuild, err = store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
185+
_, err = store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
190186
ID: workspaceBuildID,
191187
CreatedAt: now,
192188
UpdatedAt: now,
193189
WorkspaceID: workspace.ID,
194190
TemplateVersionID: priorHistory.TemplateVersionID,
195-
BeforeID: priorHistoryID,
191+
BuildNumber: priorBuildNumber + 1,
196192
Name: namesgenerator.GetRandomName(1),
197193
ProvisionerState: priorHistory.ProvisionerState,
198194
InitiatorID: workspace.OwnerID,
@@ -202,21 +198,5 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
202198
if err != nil {
203199
return xerrors.Errorf("insert workspace build: %w", err)
204200
}
205-
206-
if priorHistoryID.Valid {
207-
// Update the prior history entries "after" column.
208-
err = store.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
209-
ID: priorHistory.ID,
210-
ProvisionerState: priorHistory.ProvisionerState,
211-
UpdatedAt: now,
212-
AfterID: uuid.NullUUID{
213-
UUID: newWorkspaceBuild.ID,
214-
Valid: true,
215-
},
216-
})
217-
if err != nil {
218-
return xerrors.Errorf("update prior workspace build: %w", err)
219-
}
220-
}
221201
return nil
222202
}

coderd/autobuild/executor/lifecycle_executor_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,17 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
419419
require.NotEqual(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected a workspace build to occur")
420420
require.Equal(t, codersdk.ProvisionerJobSucceeded, ws.LatestBuild.Job.Status, "expected provisioner job to have succeeded")
421421
require.Equal(t, database.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected latest transition to be start")
422-
builds, err := client.WorkspaceBuilds(ctx, ws.ID)
422+
builds, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{WorkspaceID: ws.ID})
423423
require.NoError(t, err, "fetch list of workspace builds from primary")
424424
// One build to start, one stop transition, and one autostart. No more.
425+
require.Equal(t, database.WorkspaceTransitionStart, builds[0].Transition)
426+
require.Equal(t, database.WorkspaceTransitionStop, builds[1].Transition)
427+
require.Equal(t, database.WorkspaceTransitionStart, builds[2].Transition)
425428
require.Len(t, builds, 3, "unexpected number of builds for workspace from primary")
429+
430+
// Builds are returned most recent first.
431+
require.True(t, builds[0].CreatedAt.After(builds[1].CreatedAt))
432+
require.True(t, builds[1].CreatedAt.After(builds[2].CreatedAt))
426433
}
427434

428435
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client) codersdk.Workspace {

coderd/coderdtest/coderdtest.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,20 @@ func CreateTemplateVersion(t *testing.T, client *codersdk.Client, organizationID
295295
return templateVersion
296296
}
297297

298+
// CreateWorkspaceBuild creates a workspace build for the given workspace and transition.
299+
func CreateWorkspaceBuild(
300+
t *testing.T,
301+
client *codersdk.Client,
302+
workspace codersdk.Workspace,
303+
transition database.WorkspaceTransition) codersdk.WorkspaceBuild {
304+
req := codersdk.CreateWorkspaceBuildRequest{
305+
Transition: transition,
306+
}
307+
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, req)
308+
require.NoError(t, err)
309+
return build
310+
}
311+
298312
// CreateTemplate creates a template with the "echo" provisioner for
299313
// compatibility with testing. The name assigned is randomly generated.
300314
func CreateTemplate(t *testing.T, client *codersdk.Client, organization uuid.UUID, version uuid.UUID) codersdk.Template {

coderd/database/databasefake/databasefake.go

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -441,50 +441,100 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI
441441
return database.WorkspaceBuild{}, sql.ErrNoRows
442442
}
443443

444-
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
444+
func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
445445
q.mutex.RLock()
446446
defer q.mutex.RUnlock()
447447

448+
var row database.WorkspaceBuild
449+
var buildNum int32
448450
for _, workspaceBuild := range q.workspaceBuilds {
449-
if workspaceBuild.WorkspaceID.String() != workspaceID.String() {
450-
continue
451-
}
452-
if !workspaceBuild.AfterID.Valid {
453-
return workspaceBuild, nil
451+
if workspaceBuild.WorkspaceID.String() == workspaceID.String() && workspaceBuild.BuildNumber > buildNum {
452+
row = workspaceBuild
453+
buildNum = workspaceBuild.BuildNumber
454454
}
455455
}
456-
return database.WorkspaceBuild{}, sql.ErrNoRows
456+
if buildNum == 0 {
457+
return database.WorkspaceBuild{}, sql.ErrNoRows
458+
}
459+
return row, nil
457460
}
458461

459-
func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) {
462+
func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) {
460463
q.mutex.RLock()
461464
defer q.mutex.RUnlock()
462465

463-
builds := make([]database.WorkspaceBuild, 0)
466+
builds := make(map[uuid.UUID]database.WorkspaceBuild)
467+
buildNumbers := make(map[uuid.UUID]int32)
464468
for _, workspaceBuild := range q.workspaceBuilds {
465469
for _, id := range ids {
466-
if id.String() != workspaceBuild.WorkspaceID.String() {
467-
continue
470+
if id.String() == workspaceBuild.WorkspaceID.String() && workspaceBuild.BuildNumber > buildNumbers[id] {
471+
builds[id] = workspaceBuild
472+
buildNumbers[id] = workspaceBuild.BuildNumber
468473
}
469-
builds = append(builds, workspaceBuild)
470474
}
471475
}
472-
if len(builds) == 0 {
476+
var returnBuilds []database.WorkspaceBuild
477+
for i, n := range buildNumbers {
478+
if n > 0 {
479+
b := builds[i]
480+
returnBuilds = append(returnBuilds, b)
481+
}
482+
}
483+
if len(returnBuilds) == 0 {
473484
return nil, sql.ErrNoRows
474485
}
475-
return builds, nil
486+
return returnBuilds, nil
476487
}
477488

478-
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceBuild, error) {
489+
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context,
490+
params database.GetWorkspaceBuildByWorkspaceIDParams) ([]database.WorkspaceBuild, error) {
479491
q.mutex.RLock()
480492
defer q.mutex.RUnlock()
481493

482494
history := make([]database.WorkspaceBuild, 0)
483495
for _, workspaceBuild := range q.workspaceBuilds {
484-
if workspaceBuild.WorkspaceID.String() == workspaceID.String() {
496+
if workspaceBuild.WorkspaceID.String() == params.WorkspaceID.String() {
485497
history = append(history, workspaceBuild)
486498
}
487499
}
500+
501+
// Order by build_number
502+
slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool {
503+
// use greater than since we want descending order
504+
return a.BuildNumber > b.BuildNumber
505+
})
506+
507+
if params.AfterID != uuid.Nil {
508+
found := false
509+
for i, v := range history {
510+
if v.ID == params.AfterID {
511+
// We want to return all builds after index i.
512+
history = history[i+1:]
513+
found = true
514+
break
515+
}
516+
}
517+
518+
// If no builds after the time, then we return an empty list.
519+
if !found {
520+
return nil, sql.ErrNoRows
521+
}
522+
}
523+
524+
if params.OffsetOpt > 0 {
525+
if int(params.OffsetOpt) > len(history)-1 {
526+
return nil, sql.ErrNoRows
527+
}
528+
history = history[params.OffsetOpt:]
529+
}
530+
531+
if params.LimitOpt > 0 {
532+
if int(params.LimitOpt) > len(history) {
533+
params.LimitOpt = int32(len(history))
534+
}
535+
history = history[:params.LimitOpt]
536+
}
537+
488538
if len(history) == 0 {
489539
return nil, sql.ErrNoRows
490540
}
@@ -1429,7 +1479,7 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser
14291479
WorkspaceID: arg.WorkspaceID,
14301480
Name: arg.Name,
14311481
TemplateVersionID: arg.TemplateVersionID,
1432-
BeforeID: arg.BeforeID,
1482+
BuildNumber: arg.BuildNumber,
14331483
Transition: arg.Transition,
14341484
InitiatorID: arg.InitiatorID,
14351485
JobID: arg.JobID,
@@ -1641,7 +1691,6 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U
16411691
continue
16421692
}
16431693
workspaceBuild.UpdatedAt = arg.UpdatedAt
1644-
workspaceBuild.AfterID = arg.AfterID
16451694
workspaceBuild.ProvisionerState = arg.ProvisionerState
16461695
q.workspaceBuilds[index] = workspaceBuild
16471696
return nil

coderd/database/dump.sql

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/migrations/000004_jobs.up.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,14 @@ CREATE TABLE workspace_builds (
165165
workspace_id uuid NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
166166
template_version_id uuid NOT NULL REFERENCES template_versions (id) ON DELETE CASCADE,
167167
name varchar(64) NOT NULL,
168-
before_id uuid,
169-
after_id uuid,
168+
build_number integer NOT NULL,
170169
transition workspace_transition NOT NULL,
171170
initiator_id uuid NOT NULL,
172171
-- State stored by the provisioner
173172
provisioner_state bytea,
174173
-- Job ID of the action
175174
job_id uuid NOT NULL UNIQUE REFERENCES provisioner_jobs (id) ON DELETE CASCADE,
176175
PRIMARY KEY (id),
177-
UNIQUE(workspace_id, name)
176+
UNIQUE(workspace_id, name),
177+
UNIQUE(workspace_id, build_number)
178178
);

coderd/database/models.go

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/postgres/postgres_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ func TestMain(m *testing.M) {
1717
goleak.VerifyTestMain(m)
1818
}
1919

20+
// nolint:paralleltest
2021
func TestPostgres(t *testing.T) {
21-
t.Parallel()
22+
// postgres.Open() seems to be creating race conditions when run in parallel.
23+
// t.Parallel()
2224

2325
if testing.Short() {
2426
t.Skip()

coderd/database/pubsub_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ func TestPubsub(t *testing.T) {
2222
return
2323
}
2424

25+
// nolint:paralleltest
2526
t.Run("Postgres", func(t *testing.T) {
26-
t.Parallel()
27+
// postgres.Open() seems to be creating race conditions when run in parallel.
28+
// t.Parallel()
2729
ctx, cancelFunc := context.WithCancel(context.Background())
2830
defer cancelFunc()
2931

@@ -52,8 +54,10 @@ func TestPubsub(t *testing.T) {
5254
assert.Equal(t, string(message), data)
5355
})
5456

57+
// nolint:paralleltest
5558
t.Run("PostgresCloseCancel", func(t *testing.T) {
56-
t.Parallel()
59+
// postgres.Open() seems to be creating race conditions when run in parallel.
60+
// t.Parallel()
5761
ctx, cancelFunc := context.WithCancel(context.Background())
5862
defer cancelFunc()
5963
connectionURL, closePg, err := postgres.Open()

coderd/database/querier.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)