mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add workspace restart functionality to API (#25757)
This models restart as durable orchestration of existing stop and start workspace builds instead of adding a new restart transition. Keeping restart as two existing transitions preserves the current build/provisioner model. The child start build is created only after the parent stop build succeeds, rather than being inserted immediately in a pending state. That keeps `workspace_builds` aligned with actual provisioner-ready work and avoids introducing a second pending-build lifecycle that the provisioner and build acquisition paths would need to understand. Refs: https://linear.app/codercom/issue/PLAT-143
This commit is contained in:
@@ -39,6 +39,11 @@ const (
|
||||
// long enough to cover the maximum interval of a heartbeat event (currently
|
||||
// 1 hour) plus some buffer.
|
||||
maxTelemetryHeartbeatAge = 24 * time.Hour
|
||||
// Operational handoff state; terminal rows are kept for debugging, then
|
||||
// purged.
|
||||
workspaceBuildOrchestrationTerminalRetention = 24 * time.Hour
|
||||
// Batch size for workspace build orchestration deletion.
|
||||
workspaceBuildOrchestrationsBatchSize = 10000
|
||||
// Chat and chat file batch sizes stay smaller than audit/connection
|
||||
// log batches because chat_files rows carry bytea blobs.
|
||||
chatsBatchSize = 1000
|
||||
@@ -275,6 +280,15 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
|
||||
}
|
||||
}
|
||||
|
||||
deleteOldWorkspaceBuildOrchestrationsBefore := start.Add(-workspaceBuildOrchestrationTerminalRetention)
|
||||
purgedWorkspaceBuildOrchestrations, err := tx.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
|
||||
BeforeTime: deleteOldWorkspaceBuildOrchestrationsBefore,
|
||||
LimitCount: workspaceBuildOrchestrationsBatchSize,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to delete old workspace build orchestrations: %w", err)
|
||||
}
|
||||
|
||||
var purgedChats, purgedChatFiles, purgedChatDebugRuns int64
|
||||
if purgeChats {
|
||||
purgedChats, purgedChatFiles, err = i.purgeChatsInTx(ctx, tx, start, chatRetentionDays)
|
||||
@@ -304,6 +318,7 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
|
||||
slog.F("audit_logs", purgedAuditLogs),
|
||||
slog.F("boundary_logs", purgedBoundaryLogs),
|
||||
slog.F("boundary_sessions", purgedBoundarySessions),
|
||||
slog.F("workspace_build_orchestrations", purgedWorkspaceBuildOrchestrations),
|
||||
slog.F("chats", purgedChats),
|
||||
slog.F("chat_files", purgedChatFiles),
|
||||
slog.F("chat_debug_runs", purgedChatDebugRuns),
|
||||
@@ -318,6 +333,7 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
|
||||
i.recordsPurged.WithLabelValues("audit_logs").Add(float64(purgedAuditLogs))
|
||||
i.recordsPurged.WithLabelValues("boundary_logs").Add(float64(purgedBoundaryLogs))
|
||||
i.recordsPurged.WithLabelValues("boundary_sessions").Add(float64(purgedBoundarySessions))
|
||||
i.recordsPurged.WithLabelValues("workspace_build_orchestrations").Add(float64(purgedWorkspaceBuildOrchestrations))
|
||||
i.recordsPurged.WithLabelValues("chats").Add(float64(purgedChats))
|
||||
i.recordsPurged.WithLabelValues("chat_debug_runs").Add(float64(purgedChatDebugRuns))
|
||||
i.recordsPurged.WithLabelValues("chat_files").Add(float64(purgedChatFiles))
|
||||
|
||||
@@ -129,6 +129,11 @@ func TestMetrics(t *testing.T) {
|
||||
})
|
||||
require.GreaterOrEqual(t, auditLogs, 0)
|
||||
|
||||
workspaceBuildOrchestrations := promhelp.CounterValue(t, reg, "coderd_dbpurge_records_purged_total", prometheus.Labels{
|
||||
"record_type": "workspace_build_orchestrations",
|
||||
})
|
||||
require.GreaterOrEqual(t, workspaceBuildOrchestrations, 0)
|
||||
|
||||
chats := promhelp.CounterValue(t, reg, "coderd_dbpurge_records_purged_total", prometheus.Labels{
|
||||
"record_type": "chats",
|
||||
})
|
||||
@@ -247,6 +252,7 @@ func TestMetrics(t *testing.T) {
|
||||
mDB.EXPECT().DeleteOldNotificationMessages(gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().ExpirePrebuildsAPIKeys(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldTelemetryLocks(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldWorkspaceBuildOrchestrations(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldChatDebugRuns(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatDebugRunsParams{})).Return(int64(0), nil).MinTimes(1)
|
||||
mDB.EXPECT().InTx(gomock.Any(), database.DefaultTXOptions().WithID("db_purge")).
|
||||
@@ -297,6 +303,7 @@ func TestMetrics(t *testing.T) {
|
||||
mDB.EXPECT().DeleteOldNotificationMessages(gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().ExpirePrebuildsAPIKeys(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldTelemetryLocks(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldWorkspaceBuildOrchestrations(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mDB.EXPECT().DeleteOldChats(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatsParams{})).Return(int64(0), nil).MinTimes(1)
|
||||
mDB.EXPECT().DeleteOldChatFiles(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatFilesParams{})).Return(int64(0), nil).MinTimes(1)
|
||||
@@ -1117,6 +1124,146 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "it should delete old telemetry heartbeats")
|
||||
}
|
||||
|
||||
func TestDeleteOldWorkspaceBuildOrchestrations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
db, _, rawDB := dbtestutil.NewDBWithSQLDB(t)
|
||||
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
})
|
||||
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: org.ID,
|
||||
JobID: versionJob.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
template := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
ActiveVersionID: version.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
|
||||
now := dbtime.Now()
|
||||
cutoff := now.Add(-24 * time.Hour)
|
||||
buildTime := cutoff.Add(-time.Hour)
|
||||
oldCompletedTime := cutoff.Add(-3 * time.Minute)
|
||||
oldFailedTime := cutoff.Add(-2 * time.Minute)
|
||||
oldCanceledTime := cutoff.Add(-time.Minute)
|
||||
oldPendingTime := cutoff.Add(-time.Minute)
|
||||
recentTime := cutoff.Add(time.Minute)
|
||||
|
||||
createBuild := func(buildNumber int32, createdAt time.Time) database.WorkspaceBuild {
|
||||
return mustCreateWorkspaceBuild(t, db, org, version, workspace.ID, createdAt, buildNumber)
|
||||
}
|
||||
insertOrchestration := func(parentBuild database.WorkspaceBuild, updatedAt time.Time) database.WorkspaceBuildOrchestration {
|
||||
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: updatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ParentBuildID: parentBuild.ID,
|
||||
ChildTransition: database.WorkspaceTransitionStart,
|
||||
ChildRichParameterValues: json.RawMessage("[]"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return orchestration
|
||||
}
|
||||
|
||||
// Given: old terminal orchestration rows (completed, failed,
|
||||
// canceled), an old pending row, and a recent terminal row.
|
||||
oldCompletedParent := createBuild(1, buildTime)
|
||||
oldCompletedChild := createBuild(2, buildTime)
|
||||
oldCompleted := insertOrchestration(oldCompletedParent, oldCompletedTime)
|
||||
_, err := db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
|
||||
ID: oldCompleted.ID,
|
||||
ChildBuildID: uuid.NullUUID{UUID: oldCompletedChild.ID, Valid: true},
|
||||
UpdatedAt: oldCompletedTime,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
oldFailedParent := createBuild(3, buildTime)
|
||||
oldFailed := insertOrchestration(oldFailedParent, oldFailedTime)
|
||||
_, err = db.UpdateWorkspaceBuildOrchestrationFailedByID(ctx, database.UpdateWorkspaceBuildOrchestrationFailedByIDParams{
|
||||
ID: oldFailed.ID,
|
||||
Error: sql.NullString{String: "failed", Valid: true},
|
||||
UpdatedAt: oldFailedTime,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
oldCanceledParent := createBuild(4, buildTime)
|
||||
oldCanceled := insertOrchestration(oldCanceledParent, oldCanceledTime)
|
||||
_, err = db.UpdateWorkspaceBuildOrchestrationCanceledByID(ctx, database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams{
|
||||
ID: oldCanceled.ID,
|
||||
UpdatedAt: oldCanceledTime,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
oldPendingParent := createBuild(5, buildTime)
|
||||
oldPending := insertOrchestration(oldPendingParent, oldPendingTime)
|
||||
|
||||
recentCompletedParent := createBuild(6, buildTime)
|
||||
recentCompletedChild := createBuild(7, buildTime)
|
||||
recentCompleted := insertOrchestration(recentCompletedParent, recentTime)
|
||||
_, err = db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
|
||||
ID: recentCompleted.ID,
|
||||
ChildBuildID: uuid.NullUUID{UUID: recentCompletedChild.ID, Valid: true},
|
||||
UpdatedAt: recentTime,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: old workspace build orchestrations are deleted with LimitCount 1
|
||||
deleted, err := db.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
|
||||
BeforeTime: cutoff,
|
||||
LimitCount: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, deleted)
|
||||
|
||||
// Then: only the oldest terminal row is deleted.
|
||||
assertOrchestrationDeleted(ctx, t, rawDB, oldCompletedParent.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, oldFailedParent.ID, oldFailed.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, oldCanceledParent.ID, oldCanceled.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, oldPendingParent.ID, oldPending.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, recentCompletedParent.ID, recentCompleted.ID)
|
||||
|
||||
// When: old workspace build orchestrations are deleted again.
|
||||
deleted, err = db.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
|
||||
BeforeTime: cutoff,
|
||||
LimitCount: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, deleted)
|
||||
|
||||
// Then: the remaining old terminal rows are deleted.
|
||||
assertOrchestrationDeleted(ctx, t, rawDB, oldFailedParent.ID)
|
||||
assertOrchestrationDeleted(ctx, t, rawDB, oldCanceledParent.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, oldPendingParent.ID, oldPending.ID)
|
||||
assertOrchestrationExists(ctx, t, rawDB, recentCompletedParent.ID, recentCompleted.ID)
|
||||
}
|
||||
|
||||
func assertOrchestrationDeleted(ctx context.Context, t *testing.T, rawDB *sql.DB, parentBuildID uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
_, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuildID)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func assertOrchestrationExists(ctx context.Context, t *testing.T, rawDB *sql.DB, parentBuildID uuid.UUID, orchestrationID uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuildID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, orchestrationID, orchestration.ID)
|
||||
}
|
||||
|
||||
func TestDeleteOldConnectionLogs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user