mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: automatically stop workspaces based on failure_ttl (#7989)
This commit is contained in:
@@ -1717,8 +1717,8 @@ func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesP
|
||||
return q.db.GetAuthorizedWorkspaces(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
return q.db.GetWorkspacesEligibleForAutoStartStop(ctx, now)
|
||||
func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
return q.db.GetWorkspacesEligibleForTransition(ctx, now)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
|
||||
@@ -20,9 +20,11 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
@@ -3432,7 +3434,7 @@ func (q *fakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspa
|
||||
return workspaceRows, err
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
func (q *fakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
@@ -3452,6 +3454,15 @@ func (q *fakeQuerier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context,
|
||||
workspaces = append(workspaces, workspace)
|
||||
continue
|
||||
}
|
||||
|
||||
job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
|
||||
}
|
||||
if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed {
|
||||
workspaces = append(workspaces, workspace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
|
||||
@@ -992,9 +992,9 @@ func (m metricsStore) GetWorkspaces(ctx context.Context, arg database.GetWorkspa
|
||||
return workspaces, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
func (m metricsStore) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) {
|
||||
start := time.Now()
|
||||
workspaces, err := m.s.GetWorkspacesEligibleForAutoStartStop(ctx, now)
|
||||
workspaces, err := m.s.GetWorkspacesEligibleForTransition(ctx, now)
|
||||
m.queryLatencies.WithLabelValues("GetWorkspacesEligibleForAutoStartStop").Observe(time.Since(start).Seconds())
|
||||
return workspaces, err
|
||||
}
|
||||
|
||||
@@ -2007,19 +2007,19 @@ func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaces", reflect.TypeOf((*MockStore)(nil).GetWorkspaces), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetWorkspacesEligibleForAutoStartStop mocks base method.
|
||||
func (m *MockStore) GetWorkspacesEligibleForAutoStartStop(arg0 context.Context, arg1 time.Time) ([]database.Workspace, error) {
|
||||
// GetWorkspacesEligibleForTransition mocks base method.
|
||||
func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.Workspace, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetWorkspacesEligibleForAutoStartStop", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "GetWorkspacesEligibleForTransition", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.Workspace)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetWorkspacesEligibleForAutoStartStop indicates an expected call of GetWorkspacesEligibleForAutoStartStop.
|
||||
func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForAutoStartStop(arg0, arg1 interface{}) *gomock.Call {
|
||||
// GetWorkspacesEligibleForTransition indicates an expected call of GetWorkspacesEligibleForTransition.
|
||||
func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForAutoStartStop", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForAutoStartStop), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), arg0, arg1)
|
||||
}
|
||||
|
||||
// InTx mocks base method.
|
||||
|
||||
@@ -171,7 +171,7 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
|
||||
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error)
|
||||
GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]Workspace, error)
|
||||
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
// We use the organization_id as the id
|
||||
// for simplicity since all users is
|
||||
|
||||
@@ -8505,13 +8505,15 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesEligibleForAutoStartStop = `-- name: GetWorkspacesEligibleForAutoStartStop :many
|
||||
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN
|
||||
workspace_builds ON workspace_builds.workspace_id = workspaces.id
|
||||
INNER JOIN
|
||||
provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
|
||||
WHERE
|
||||
workspace_builds.build_number = (
|
||||
SELECT
|
||||
@@ -8541,12 +8543,20 @@ WHERE
|
||||
(
|
||||
workspace_builds.transition = 'stop'::workspace_transition AND
|
||||
workspaces.autostart_schedule IS NOT NULL
|
||||
) OR
|
||||
|
||||
-- If the workspace's most recent job resulted in an error
|
||||
-- it may be eligible for failed stop.
|
||||
(
|
||||
provisioner_jobs.error IS NOT NULL AND
|
||||
provisioner_jobs.error != '' AND
|
||||
workspace_builds.transition = 'start'::workspace_transition
|
||||
)
|
||||
)
|
||||
) AND workspaces.deleted = 'false'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForAutoStartStop, now)
|
||||
func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForTransition, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -405,13 +405,15 @@ SELECT
|
||||
stopped_workspaces.count AS stopped_workspaces
|
||||
FROM pending_workspaces, building_workspaces, running_workspaces, failed_workspaces, stopped_workspaces;
|
||||
|
||||
-- name: GetWorkspacesEligibleForAutoStartStop :many
|
||||
-- name: GetWorkspacesEligibleForTransition :many
|
||||
SELECT
|
||||
workspaces.*
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN
|
||||
workspace_builds ON workspace_builds.workspace_id = workspaces.id
|
||||
INNER JOIN
|
||||
provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
|
||||
WHERE
|
||||
workspace_builds.build_number = (
|
||||
SELECT
|
||||
@@ -441,5 +443,13 @@ WHERE
|
||||
(
|
||||
workspace_builds.transition = 'stop'::workspace_transition AND
|
||||
workspaces.autostart_schedule IS NOT NULL
|
||||
) OR
|
||||
|
||||
-- If the workspace's most recent job resulted in an error
|
||||
-- it may be eligible for failed stop.
|
||||
(
|
||||
provisioner_jobs.error IS NOT NULL AND
|
||||
provisioner_jobs.error != '' AND
|
||||
workspace_builds.transition = 'start'::workspace_transition
|
||||
)
|
||||
);
|
||||
) AND workspaces.deleted = 'false';
|
||||
|
||||
Reference in New Issue
Block a user