Skip to content

Commit bd139f3

Browse files
johnstcnmafredri
andauthored
fix(coderd/provisionerdserver): workaround lack of coder_ai_task resource on stop transition (#19560)
This works around the issue where a task may "disappear" on stop. Re-using the previous value of `has_ai_task` and `sidebar_app_id` on a stop transition. --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parent 8083d9d commit bd139f3

File tree

2 files changed

+114
-1
lines changed

2 files changed

+114
-1
lines changed

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,37 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
19951995
sidebarAppID = uuid.NullUUID{UUID: id, Valid: true}
19961996
}
19971997

1998+
// This is a hacky workaround for the issue with tasks 'disappearing' on stop:
1999+
// reuse has_ai_task and sidebar_app_id from the previous build.
2000+
// This workaround should be removed as soon as possible.
2001+
if workspaceBuild.Transition == database.WorkspaceTransitionStop && workspaceBuild.BuildNumber > 1 {
2002+
if prevBuild, err := s.Database.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
2003+
WorkspaceID: workspaceBuild.WorkspaceID,
2004+
BuildNumber: workspaceBuild.BuildNumber - 1,
2005+
}); err == nil {
2006+
hasAITask = prevBuild.HasAITask.Bool
2007+
sidebarAppID = prevBuild.AITaskSidebarAppID
2008+
warnUnknownSidebarAppID = false
2009+
s.Logger.Debug(ctx, "task workaround: reused has_ai_task and sidebar_app_id from previous build to keep track of task",
2010+
slog.F("job_id", job.ID.String()),
2011+
slog.F("build_number", prevBuild.BuildNumber),
2012+
slog.F("workspace_id", workspace.ID),
2013+
slog.F("workspace_build_id", workspaceBuild.ID),
2014+
slog.F("transition", string(workspaceBuild.Transition)),
2015+
slog.F("sidebar_app_id", sidebarAppID.UUID),
2016+
slog.F("has_ai_task", hasAITask),
2017+
)
2018+
} else {
2019+
s.Logger.Error(ctx, "task workaround: tracking via has_ai_task and sidebar_app from previous build failed",
2020+
slog.Error(err),
2021+
slog.F("job_id", job.ID.String()),
2022+
slog.F("workspace_id", workspace.ID),
2023+
slog.F("workspace_build_id", workspaceBuild.ID),
2024+
slog.F("transition", string(workspaceBuild.Transition)),
2025+
)
2026+
}
2027+
}
2028+
19982029
if warnUnknownSidebarAppID {
19992030
// Ref: https://github.com/coder/coder/issues/18776
20002031
// This can happen for a number of reasons:

coderd/provisionerdserver/provisionerdserver_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2842,9 +2842,12 @@ func TestCompleteJob(t *testing.T) {
28422842
// has_ai_task has a default value of nil, but once the workspace build completes it will have a value;
28432843
// it is set to "true" if the related template has any coder_ai_task resources defined, and its sidebar app ID
28442844
// will be set as well in that case.
2845+
// HACK(johnstcn): we also set it to "true" if any _previous_ workspace builds ever had it set to "true".
2846+
// This is to avoid tasks "disappearing" when you stop them.
28452847
t.Run("WorkspaceBuild", func(t *testing.T) {
28462848
type testcase struct {
28472849
name string
2850+
seedFunc func(context.Context, testing.TB, database.Store) error // If you need to insert other resources
28482851
transition database.WorkspaceTransition
28492852
input *proto.CompletedJob_WorkspaceBuild
28502853
expectHasAiTask bool
@@ -2944,6 +2947,17 @@ func TestCompleteJob(t *testing.T) {
29442947
expectHasAiTask: true,
29452948
expectUsageEvent: false,
29462949
},
2950+
{
2951+
name: "current build does not have ai task but previous build did",
2952+
seedFunc: seedPreviousWorkspaceStartWithAITask,
2953+
transition: database.WorkspaceTransitionStop,
2954+
input: &proto.CompletedJob_WorkspaceBuild{
2955+
AiTasks: []*sdkproto.AITask{},
2956+
Resources: []*sdkproto.Resource{},
2957+
},
2958+
expectHasAiTask: true,
2959+
expectUsageEvent: false,
2960+
},
29472961
} {
29482962
t.Run(tc.name, func(t *testing.T) {
29492963
t.Parallel()
@@ -2980,6 +2994,9 @@ func TestCompleteJob(t *testing.T) {
29802994
})
29812995

29822996
ctx := testutil.Context(t, testutil.WaitShort)
2997+
if tc.seedFunc != nil {
2998+
require.NoError(t, tc.seedFunc(ctx, t, db))
2999+
}
29833000

29843001
buildJobID := uuid.New()
29853002
wsBuildID := uuid.New()
@@ -2999,8 +3016,13 @@ func TestCompleteJob(t *testing.T) {
29993016
Tags: pd.Tags,
30003017
})
30013018
require.NoError(t, err)
3019+
var buildNum int32
3020+
if latestBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceTable.ID); err == nil {
3021+
buildNum = latestBuild.BuildNumber
3022+
}
30023023
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
30033024
ID: wsBuildID,
3025+
BuildNumber: buildNum + 1,
30043026
JobID: buildJobID,
30053027
WorkspaceID: workspaceTable.ID,
30063028
TemplateVersionID: version.ID,
@@ -3038,7 +3060,7 @@ func TestCompleteJob(t *testing.T) {
30383060
require.True(t, build.HasAITask.Valid) // We ALWAYS expect a value to be set, therefore not nil, i.e. valid = true.
30393061
require.Equal(t, tc.expectHasAiTask, build.HasAITask.Bool)
30403062

3041-
if tc.expectHasAiTask {
3063+
if tc.expectHasAiTask && build.Transition != database.WorkspaceTransitionStop {
30423064
require.Equal(t, sidebarAppID, build.AITaskSidebarAppID.UUID.String())
30433065
}
30443066

@@ -4244,3 +4266,63 @@ func (f *fakeUsageInserter) InsertDiscreteUsageEvent(_ context.Context, _ databa
42444266
f.collectedEvents = append(f.collectedEvents, event)
42454267
return nil
42464268
}
4269+
4270+
func seedPreviousWorkspaceStartWithAITask(ctx context.Context, t testing.TB, db database.Store) error {
4271+
t.Helper()
4272+
// If the below looks slightly convoluted, that's because it is.
4273+
// The workspace doesn't yet have a latest build, so querying all
4274+
// workspaces will fail.
4275+
tpls, err := db.GetTemplates(ctx)
4276+
if err != nil {
4277+
return xerrors.Errorf("seedFunc: get template: %w", err)
4278+
}
4279+
if len(tpls) != 1 {
4280+
return xerrors.Errorf("seedFunc: expected exactly one template, got %d", len(tpls))
4281+
}
4282+
ws, err := db.GetWorkspacesByTemplateID(ctx, tpls[0].ID)
4283+
if err != nil {
4284+
return xerrors.Errorf("seedFunc: get workspaces: %w", err)
4285+
}
4286+
if len(ws) != 1 {
4287+
return xerrors.Errorf("seedFunc: expected exactly one workspace, got %d", len(ws))
4288+
}
4289+
w := ws[0]
4290+
prevJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
4291+
OrganizationID: w.OrganizationID,
4292+
InitiatorID: w.OwnerID,
4293+
Type: database.ProvisionerJobTypeWorkspaceBuild,
4294+
})
4295+
tvs, err := db.GetTemplateVersionsByTemplateID(ctx, database.GetTemplateVersionsByTemplateIDParams{
4296+
TemplateID: tpls[0].ID,
4297+
})
4298+
if err != nil {
4299+
return xerrors.Errorf("seedFunc: get template version: %w", err)
4300+
}
4301+
if len(tvs) != 1 {
4302+
return xerrors.Errorf("seedFunc: expected exactly one template version, got %d", len(tvs))
4303+
}
4304+
if tpls[0].ActiveVersionID == uuid.Nil {
4305+
return xerrors.Errorf("seedFunc: active version id is nil")
4306+
}
4307+
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
4308+
JobID: prevJob.ID,
4309+
})
4310+
agt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
4311+
ResourceID: res.ID,
4312+
})
4313+
wa := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{
4314+
AgentID: agt.ID,
4315+
})
4316+
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
4317+
BuildNumber: 1,
4318+
HasAITask: sql.NullBool{Valid: true, Bool: true},
4319+
AITaskSidebarAppID: uuid.NullUUID{Valid: true, UUID: wa.ID},
4320+
ID: w.ID,
4321+
InitiatorID: w.OwnerID,
4322+
JobID: prevJob.ID,
4323+
TemplateVersionID: tvs[0].ID,
4324+
Transition: database.WorkspaceTransitionStart,
4325+
WorkspaceID: w.ID,
4326+
})
4327+
return nil
4328+
}

0 commit comments

Comments
 (0)