mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: improve build deadline code (#19203)
- Adds/improves a lot of comments to make the autostop calculation code clearer - Changes the behavior of the enterprise template schedule store to match the behavior of the workspace TTL endpoint when the new TTL is zero - Fixes a bug in the workspace TTL endpoint where it could unset the build deadline, even though a max_deadline was specified - Adds a new constraint to the workspace_builds table that enforces the deadline is non-zero and below the max_deadline if it is set - Adds CHECK constraint enum generation to scripts/dbgen, used for testing the above constraint - Adds Dean and Danielle as CODEOWNERS for the autostop calculation code
This commit is contained in:
@@ -29,3 +29,8 @@ site/src/api/countriesGenerated.ts
|
|||||||
site/src/api/rbacresourcesGenerated.ts
|
site/src/api/rbacresourcesGenerated.ts
|
||||||
site/src/api/typesGenerated.ts
|
site/src/api/typesGenerated.ts
|
||||||
site/CLAUDE.md
|
site/CLAUDE.md
|
||||||
|
|
||||||
|
# The blood and guts of the autostop algorithm, which is quite complex and
|
||||||
|
# requires elite ball knowledge of most of the scheduling code to make changes
|
||||||
|
# without inadvertently affecting other parts of the codebase.
|
||||||
|
coderd/schedule/autostop.go @deansheather @DanielleMaywood
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Code generated by scripts/dbgen/main.go. DO NOT EDIT.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// CheckConstraint represents a named check constraint on a table.
|
||||||
|
type CheckConstraint string
|
||||||
|
|
||||||
|
// CheckConstraint enums.
|
||||||
|
const (
|
||||||
|
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
|
||||||
|
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
|
||||||
|
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
|
||||||
|
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
|
||||||
|
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
|
||||||
|
CheckWorkspaceBuildsAiTaskSidebarAppIDRequired CheckConstraint = "workspace_builds_ai_task_sidebar_app_id_required" // workspace_builds
|
||||||
|
CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds
|
||||||
|
)
|
||||||
Generated
+2
-1
@@ -2224,7 +2224,8 @@ CREATE TABLE workspace_builds (
|
|||||||
template_version_preset_id uuid,
|
template_version_preset_id uuid,
|
||||||
has_ai_task boolean,
|
has_ai_task boolean,
|
||||||
ai_task_sidebar_app_id uuid,
|
ai_task_sidebar_app_id uuid,
|
||||||
CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK (((((has_ai_task IS NULL) OR (has_ai_task = false)) AND (ai_task_sidebar_app_id IS NULL)) OR ((has_ai_task = true) AND (ai_task_sidebar_app_id IS NOT NULL))))
|
CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK (((((has_ai_task IS NULL) OR (has_ai_task = false)) AND (ai_task_sidebar_app_id IS NULL)) OR ((has_ai_task = true) AND (ai_task_sidebar_app_id IS NOT NULL)))),
|
||||||
|
CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone)))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE VIEW workspace_build_with_user AS
|
CREATE VIEW workspace_build_with_user AS
|
||||||
|
|||||||
@@ -59,6 +59,28 @@ func IsForeignKeyViolation(err error, foreignKeyConstraints ...ForeignKeyConstra
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCheckViolation checks if the error is due to a check violation. If one or
|
||||||
|
// more specific check constraints are given as arguments, the error must be
|
||||||
|
// caused by one of them. If no constraints are given, this function returns
|
||||||
|
// true for any check violation.
|
||||||
|
func IsCheckViolation(err error, checkConstraints ...CheckConstraint) bool {
|
||||||
|
var pqErr *pq.Error
|
||||||
|
if errors.As(err, &pqErr) {
|
||||||
|
if pqErr.Code.Name() == "check_violation" {
|
||||||
|
if len(checkConstraints) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, cc := range checkConstraints {
|
||||||
|
if pqErr.Constraint == string(cc) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsQueryCanceledError checks if the error is due to a query being canceled.
|
// IsQueryCanceledError checks if the error is due to a query being canceled.
|
||||||
func IsQueryCanceledError(err error) bool {
|
func IsQueryCanceledError(err error) bool {
|
||||||
var pqErr *pq.Error
|
var pqErr *pq.Error
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE workspace_builds
|
||||||
|
DROP CONSTRAINT workspace_builds_deadline_below_max_deadline;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- New constraint: (deadline IS NOT zero AND deadline <= max_deadline) UNLESS max_deadline is zero.
|
||||||
|
-- Unfortunately, "zero" here means `time.Time{}`...
|
||||||
|
|
||||||
|
-- Update previous builds that would fail this new constraint. This matches the
|
||||||
|
-- intended behaviour of the autostop algorithm.
|
||||||
|
UPDATE
|
||||||
|
workspace_builds
|
||||||
|
SET
|
||||||
|
deadline = max_deadline
|
||||||
|
WHERE
|
||||||
|
deadline > max_deadline
|
||||||
|
AND max_deadline != '0001-01-01 00:00:00+00';
|
||||||
|
|
||||||
|
-- Add the new constraint.
|
||||||
|
ALTER TABLE workspace_builds
|
||||||
|
ADD CONSTRAINT workspace_builds_deadline_below_max_deadline
|
||||||
|
CHECK (
|
||||||
|
(deadline != '0001-01-01 00:00:00+00'::timestamptz AND deadline <= max_deadline)
|
||||||
|
OR max_deadline = '0001-01-01 00:00:00+00'::timestamptz
|
||||||
|
);
|
||||||
@@ -6003,3 +6003,102 @@ func TestGetRunningPrebuiltWorkspaces(t *testing.T) {
|
|||||||
require.Len(t, runningPrebuilds, 1, "expected only one running prebuilt workspace")
|
require.Len(t, runningPrebuilds, 1, "expected only one running prebuilt workspace")
|
||||||
require.Equal(t, runningPrebuild.ID, runningPrebuilds[0].ID, "expected the running prebuilt workspace to be returned")
|
require.Equal(t, runningPrebuild.ID, runningPrebuilds[0].ID, "expected the running prebuilt workspace to be returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWorkspaceBuildDeadlineConstraint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
|
db, _ := dbtestutil.NewDB(t)
|
||||||
|
org := dbgen.Organization(t, db, database.Organization{})
|
||||||
|
user := dbgen.User(t, db, database.User{})
|
||||||
|
template := dbgen.Template(t, db, database.Template{
|
||||||
|
CreatedBy: user.ID,
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
})
|
||||||
|
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||||
|
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
CreatedBy: user.ID,
|
||||||
|
})
|
||||||
|
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
TemplateID: template.ID,
|
||||||
|
Name: "test-workspace",
|
||||||
|
Deleted: false,
|
||||||
|
})
|
||||||
|
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||||
|
OrganizationID: org.ID,
|
||||||
|
InitiatorID: database.PrebuildsSystemUserID,
|
||||||
|
Provisioner: database.ProvisionerTypeEcho,
|
||||||
|
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||||
|
StartedAt: sql.NullTime{Time: time.Now().Add(-time.Minute), Valid: true},
|
||||||
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
})
|
||||||
|
workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||||
|
WorkspaceID: workspace.ID,
|
||||||
|
TemplateVersionID: templateVersion.ID,
|
||||||
|
JobID: job.ID,
|
||||||
|
BuildNumber: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
deadline time.Time
|
||||||
|
maxDeadline time.Time
|
||||||
|
expectOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no deadline or max_deadline",
|
||||||
|
deadline: time.Time{},
|
||||||
|
maxDeadline: time.Time{},
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline set when max_deadline is not set",
|
||||||
|
deadline: time.Now().Add(time.Hour),
|
||||||
|
maxDeadline: time.Time{},
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline before max_deadline",
|
||||||
|
deadline: time.Now().Add(-time.Hour),
|
||||||
|
maxDeadline: time.Now().Add(time.Hour),
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline is max_deadline",
|
||||||
|
deadline: time.Now().Add(time.Hour),
|
||||||
|
maxDeadline: time.Now().Add(time.Hour),
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "deadline after max_deadline",
|
||||||
|
deadline: time.Now().Add(time.Hour),
|
||||||
|
maxDeadline: time.Now().Add(-time.Hour),
|
||||||
|
expectOK: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deadline is not set when max_deadline is set",
|
||||||
|
deadline: time.Time{},
|
||||||
|
maxDeadline: time.Now().Add(time.Hour),
|
||||||
|
expectOK: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
|
||||||
|
ID: workspaceBuild.ID,
|
||||||
|
Deadline: c.deadline,
|
||||||
|
MaxDeadline: c.maxDeadline,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
if c.expectOK {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.True(t, database.IsCheckViolation(err, database.CheckWorkspaceBuildsDeadlineBelowMaxDeadline))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1866,8 +1866,9 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
|
|||||||
Database: db,
|
Database: db,
|
||||||
TemplateScheduleStore: templateScheduleStore,
|
TemplateScheduleStore: templateScheduleStore,
|
||||||
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
||||||
Now: now,
|
// `now` is used below to set the build completion time.
|
||||||
Workspace: workspace.WorkspaceTable(),
|
WorkspaceBuildCompletedAt: now,
|
||||||
|
Workspace: workspace.WorkspaceTable(),
|
||||||
// Allowed to be the empty string.
|
// Allowed to be the empty string.
|
||||||
WorkspaceAutostart: workspace.AutostartSchedule.String,
|
WorkspaceAutostart: workspace.AutostartSchedule.String,
|
||||||
})
|
})
|
||||||
|
|||||||
+71
-47
@@ -50,8 +50,19 @@ type CalculateAutostopParams struct {
|
|||||||
// by autobuild.NextAutostart
|
// by autobuild.NextAutostart
|
||||||
WorkspaceAutostart string
|
WorkspaceAutostart string
|
||||||
|
|
||||||
Now time.Time
|
// WorkspaceBuildCompletedAt is the time when the workspace build was
|
||||||
Workspace database.WorkspaceTable
|
// completed.
|
||||||
|
//
|
||||||
|
// We always want to calculate using the build completion time, and not just
|
||||||
|
// the current time, to avoid forcing a workspace build's max_deadline being
|
||||||
|
// pushed to the next potential cron instance.
|
||||||
|
//
|
||||||
|
// E.g. if this function is called for an existing workspace build, which
|
||||||
|
// currently has a max_deadline within the next 2 hours (see leeway
|
||||||
|
// above), and the current time is passed into this function, the
|
||||||
|
// max_deadline will be updated to be much later than expected.
|
||||||
|
WorkspaceBuildCompletedAt time.Time
|
||||||
|
Workspace database.WorkspaceTable
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutostopTime struct {
|
type AutostopTime struct {
|
||||||
@@ -68,8 +79,8 @@ type AutostopTime struct {
|
|||||||
// Deadline is the time when the workspace will be stopped, as long as it
|
// Deadline is the time when the workspace will be stopped, as long as it
|
||||||
// doesn't see any new activity (such as SSH, app requests, etc.). When activity
|
// doesn't see any new activity (such as SSH, app requests, etc.). When activity
|
||||||
// is detected the deadline is bumped by the workspace's TTL (this only happens
|
// is detected the deadline is bumped by the workspace's TTL (this only happens
|
||||||
// when activity is detected and more than 20% of the TTL has passed to save
|
// when activity is detected and more than 5% of the TTL has passed to save
|
||||||
// database queries).
|
// database queries, see the ActivityBumpWorkspace query).
|
||||||
//
|
//
|
||||||
// MaxDeadline is the maximum value for deadline. The deadline cannot be bumped
|
// MaxDeadline is the maximum value for deadline. The deadline cannot be bumped
|
||||||
// past this value, so it denotes the absolute deadline that the workspace build
|
// past this value, so it denotes the absolute deadline that the workspace build
|
||||||
@@ -77,55 +88,45 @@ type AutostopTime struct {
|
|||||||
// requirement" settings and the user's "quiet hours" settings to pick a time
|
// requirement" settings and the user's "quiet hours" settings to pick a time
|
||||||
// outside of working hours.
|
// outside of working hours.
|
||||||
//
|
//
|
||||||
// Deadline is a cost saving measure, while max deadline is a
|
// Note that the deadline is checked at the database level:
|
||||||
// compliance/updating measure.
|
//
|
||||||
|
// (deadline IS NOT zero AND deadline <= max_deadline) UNLESS max_deadline is zero.
|
||||||
|
//
|
||||||
|
// Deadline is intended as a cost saving measure, not as a hard policy. It is
|
||||||
|
// derived from either the workspace's TTL or the template's TTL, depending on
|
||||||
|
// the template's policy, to ensure workspaces are stopped when they are idle.
|
||||||
|
//
|
||||||
|
// MaxDeadline is intended as a compliance policy. It is derived from the
|
||||||
|
// template's autostop requirement to cap workspace uptime and effectively force
|
||||||
|
// people to update often.
|
||||||
|
//
|
||||||
|
// Note that only the build's CURRENT deadline property influences automation in
|
||||||
|
// the autobuild package. As stated above, the MaxDeadline property is only used
|
||||||
|
// to cap the value of a build's deadline.
|
||||||
func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (AutostopTime, error) {
|
func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (AutostopTime, error) {
|
||||||
ctx, span := tracing.StartSpan(ctx,
|
ctx, span := tracing.StartSpan(ctx,
|
||||||
trace.WithAttributes(attribute.String("coder.workspace_id", params.Workspace.ID.String())),
|
trace.WithAttributes(attribute.String("coder.workspace_id", params.Workspace.ID.String())),
|
||||||
trace.WithAttributes(attribute.String("coder.template_id", params.Workspace.TemplateID.String())),
|
trace.WithAttributes(attribute.String("coder.template_id", params.Workspace.TemplateID.String())),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
db = params.Database
|
db = params.Database
|
||||||
workspace = params.Workspace
|
workspace = params.Workspace
|
||||||
now = params.Now
|
buildCompletedAt = params.WorkspaceBuildCompletedAt
|
||||||
|
|
||||||
autostop AutostopTime
|
autostop AutostopTime
|
||||||
)
|
)
|
||||||
|
|
||||||
var ttl time.Duration
|
|
||||||
if workspace.Ttl.Valid {
|
|
||||||
// When the workspace is made it copies the template's TTL, and the user
|
|
||||||
// can unset it to disable it (unless the template has
|
|
||||||
// UserAutoStopEnabled set to false, see below).
|
|
||||||
ttl = time.Duration(workspace.Ttl.Int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
if workspace.Ttl.Valid {
|
|
||||||
// When the workspace is made it copies the template's TTL, and the user
|
|
||||||
// can unset it to disable it (unless the template has
|
|
||||||
// UserAutoStopEnabled set to false, see below).
|
|
||||||
autostop.Deadline = now.Add(time.Duration(workspace.Ttl.Int64))
|
|
||||||
}
|
|
||||||
|
|
||||||
templateSchedule, err := params.TemplateScheduleStore.Get(ctx, db, workspace.TemplateID)
|
templateSchedule, err := params.TemplateScheduleStore.Get(ctx, db, workspace.TemplateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return autostop, xerrors.Errorf("get template schedule options: %w", err)
|
return autostop, xerrors.Errorf("get template schedule options: %w", err)
|
||||||
}
|
}
|
||||||
if !templateSchedule.UserAutostopEnabled {
|
|
||||||
// The user is not permitted to set their own TTL, so use the template
|
|
||||||
// default.
|
|
||||||
ttl = 0
|
|
||||||
if templateSchedule.DefaultTTL > 0 {
|
|
||||||
ttl = templateSchedule.DefaultTTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ttl := workspaceTTL(workspace, templateSchedule)
|
||||||
if ttl > 0 {
|
if ttl > 0 {
|
||||||
// Only apply non-zero TTLs.
|
// Only apply non-zero TTLs.
|
||||||
autostop.Deadline = now.Add(ttl)
|
autostop.Deadline = buildCompletedAt.Add(ttl)
|
||||||
if params.WorkspaceAutostart != "" {
|
if params.WorkspaceAutostart != "" {
|
||||||
// If the deadline passes the next autostart, we need to extend the deadline to
|
// If the deadline passes the next autostart, we need to extend the deadline to
|
||||||
// autostart + deadline. ActivityBumpWorkspace already covers this case
|
// autostart + deadline. ActivityBumpWorkspace already covers this case
|
||||||
@@ -137,14 +138,14 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
|||||||
// 3. User starts workspace at 9:45pm.
|
// 3. User starts workspace at 9:45pm.
|
||||||
// - The initial deadline is calculated to be 9:45am
|
// - The initial deadline is calculated to be 9:45am
|
||||||
// - This crosses the autostart deadline, so the deadline is extended to 9pm
|
// - This crosses the autostart deadline, so the deadline is extended to 9pm
|
||||||
nextAutostart, ok := NextAutostart(params.Now, params.WorkspaceAutostart, templateSchedule)
|
nextAutostart, ok := NextAutostart(params.WorkspaceBuildCompletedAt, params.WorkspaceAutostart, templateSchedule)
|
||||||
if ok && autostop.Deadline.After(nextAutostart) {
|
if ok && autostop.Deadline.After(nextAutostart) {
|
||||||
autostop.Deadline = nextAutostart.Add(ttl)
|
autostop.Deadline = nextAutostart.Add(ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, use the autostop_requirement algorithm.
|
// Enforce the template autostop requirement if it's configured correctly.
|
||||||
if templateSchedule.AutostopRequirement.DaysOfWeek != 0 {
|
if templateSchedule.AutostopRequirement.DaysOfWeek != 0 {
|
||||||
// The template has a autostop requirement, so determine the max deadline
|
// The template has a autostop requirement, so determine the max deadline
|
||||||
// of this workspace build.
|
// of this workspace build.
|
||||||
@@ -161,10 +162,10 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
|||||||
// workspace.
|
// workspace.
|
||||||
if userQuietHoursSchedule.Schedule != nil {
|
if userQuietHoursSchedule.Schedule != nil {
|
||||||
loc := userQuietHoursSchedule.Schedule.Location()
|
loc := userQuietHoursSchedule.Schedule.Location()
|
||||||
now := now.In(loc)
|
buildCompletedAtInLoc := buildCompletedAt.In(loc)
|
||||||
// Add the leeway here so we avoid checking today's quiet hours if
|
// Add the leeway here so we avoid checking today's quiet hours if
|
||||||
// the workspace was started <1h before midnight.
|
// the workspace was started <1h before midnight.
|
||||||
startOfStopDay := truncateMidnight(now.Add(autostopRequirementLeeway))
|
startOfStopDay := truncateMidnight(buildCompletedAtInLoc.Add(autostopRequirementLeeway))
|
||||||
|
|
||||||
// If the template schedule wants to only autostop on n-th weeks
|
// If the template schedule wants to only autostop on n-th weeks
|
||||||
// then change the startOfDay to be the Monday of the next
|
// then change the startOfDay to be the Monday of the next
|
||||||
@@ -183,7 +184,7 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
|||||||
// hour of the scheduled stop time will always bounce to the next
|
// hour of the scheduled stop time will always bounce to the next
|
||||||
// stop window).
|
// stop window).
|
||||||
checkSchedule := userQuietHoursSchedule.Schedule.Next(startOfStopDay.Add(autostopRequirementBuffer))
|
checkSchedule := userQuietHoursSchedule.Schedule.Next(startOfStopDay.Add(autostopRequirementBuffer))
|
||||||
if checkSchedule.Before(now.Add(autostopRequirementLeeway)) {
|
if checkSchedule.Before(buildCompletedAtInLoc.Add(autostopRequirementLeeway)) {
|
||||||
// Set the first stop day we try to tomorrow because today's
|
// Set the first stop day we try to tomorrow because today's
|
||||||
// schedule is too close to now or has already passed.
|
// schedule is too close to now or has already passed.
|
||||||
startOfStopDay = nextDayMidnight(startOfStopDay)
|
startOfStopDay = nextDayMidnight(startOfStopDay)
|
||||||
@@ -213,14 +214,17 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
|||||||
startOfStopDay = nextDayMidnight(startOfStopDay)
|
startOfStopDay = nextDayMidnight(startOfStopDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the startOfDay is within an hour of now, then we add an hour.
|
// If the startOfDay is within an hour of the build completion time,
|
||||||
|
// then we add an hour.
|
||||||
checkTime := startOfStopDay
|
checkTime := startOfStopDay
|
||||||
if checkTime.Before(now.Add(time.Hour)) {
|
if checkTime.Before(buildCompletedAtInLoc.Add(time.Hour)) {
|
||||||
checkTime = now.Add(time.Hour)
|
checkTime = buildCompletedAtInLoc.Add(time.Hour)
|
||||||
} else {
|
} else {
|
||||||
// If it's not within an hour of now, subtract 15 minutes to
|
// If it's not within an hour of the build completion time,
|
||||||
// give a little leeway. This prevents skipped stop events
|
// subtract 15 minutes to give a little leeway. This prevents
|
||||||
// because autostart perfectly lines up with autostop.
|
// skipped stop events because the build time (e.g. autostart
|
||||||
|
// time) perfectly lines up with the max_deadline minus the
|
||||||
|
// leeway.
|
||||||
checkTime = checkTime.Add(autostopRequirementBuffer)
|
checkTime = checkTime.Add(autostopRequirementBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,15 +242,35 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
|||||||
autostop.Deadline = autostop.MaxDeadline
|
autostop.Deadline = autostop.MaxDeadline
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!autostop.Deadline.IsZero() && autostop.Deadline.Before(now)) || (!autostop.MaxDeadline.IsZero() && autostop.MaxDeadline.Before(now)) {
|
if (!autostop.Deadline.IsZero() && autostop.Deadline.Before(buildCompletedAt)) || (!autostop.MaxDeadline.IsZero() && autostop.MaxDeadline.Before(buildCompletedAt)) {
|
||||||
// Something went wrong with the deadline calculation, so we should
|
// Something went wrong with the deadline calculation, so we should
|
||||||
// bail.
|
// bail.
|
||||||
return autostop, xerrors.Errorf("deadline calculation error, computed deadline or max deadline is in the past for workspace build: deadline=%q maxDeadline=%q now=%q", autostop.Deadline, autostop.MaxDeadline, now)
|
return autostop, xerrors.Errorf("deadline calculation error, computed deadline or max deadline is in the past for workspace build: deadline=%q maxDeadline=%q now=%q", autostop.Deadline, autostop.MaxDeadline, buildCompletedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return autostop, nil
|
return autostop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// workspaceTTL returns the TTL to use for a workspace.
|
||||||
|
//
|
||||||
|
// If the template forbids custom workspace TTLs, then we always use the
|
||||||
|
// template's configured TTL (or 0 if the template has no TTL configured).
|
||||||
|
func workspaceTTL(workspace database.WorkspaceTable, templateSchedule TemplateScheduleOptions) time.Duration {
|
||||||
|
// If the template forbids custom workspace TTLs, then we always use the
|
||||||
|
// template's configured TTL (or 0 if the template has no TTL configured).
|
||||||
|
if !templateSchedule.UserAutostopEnabled {
|
||||||
|
// This is intentionally a nested if statement because of the else if.
|
||||||
|
if templateSchedule.DefaultTTL > 0 {
|
||||||
|
return templateSchedule.DefaultTTL
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if workspace.Ttl.Valid {
|
||||||
|
return time.Duration(workspace.Ttl.Int64)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// truncateMidnight truncates a time to midnight in the time object's timezone.
|
// truncateMidnight truncates a time to midnight in the time object's timezone.
|
||||||
// t.Truncate(24 * time.Hour) truncates based on the internal time and doesn't
|
// t.Truncate(24 * time.Hour) truncates based on the internal time and doesn't
|
||||||
// factor daylight savings properly.
|
// factor daylight savings properly.
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
t.Log("saturdayMidnightAfterDstOut", saturdayMidnightAfterDstOut)
|
t.Log("saturdayMidnightAfterDstOut", saturdayMidnightAfterDstOut)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
now time.Time
|
buildCompletedAt time.Time
|
||||||
|
|
||||||
wsAutostart string
|
wsAutostart string
|
||||||
templateAutoStart schedule.TemplateAutostartRequirement
|
templateAutoStart schedule.TemplateAutostartRequirement
|
||||||
@@ -98,7 +98,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OK",
|
name: "OK",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -108,7 +108,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Delete",
|
name: "Delete",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -118,7 +118,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "WorkspaceTTL",
|
name: "WorkspaceTTL",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -128,7 +128,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateDefaultTTLIgnored",
|
name: "TemplateDefaultTTLIgnored",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: time.Hour,
|
templateDefaultTTL: time.Hour,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -138,7 +138,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "WorkspaceTTLOverridesTemplateDefaultTTL",
|
name: "WorkspaceTTLOverridesTemplateDefaultTTL",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 2 * time.Hour,
|
templateDefaultTTL: 2 * time.Hour,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -148,7 +148,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateBlockWorkspaceTTL",
|
name: "TemplateBlockWorkspaceTTL",
|
||||||
now: now,
|
buildCompletedAt: now,
|
||||||
templateAllowAutostop: false,
|
templateAllowAutostop: false,
|
||||||
templateDefaultTTL: 3 * time.Hour,
|
templateDefaultTTL: 3 * time.Hour,
|
||||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||||
@@ -158,7 +158,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirement",
|
name: "TemplateAutostopRequirement",
|
||||||
now: wednesdayMidnightUTC,
|
buildCompletedAt: wednesdayMidnightUTC,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -172,7 +172,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirement1HourSkip",
|
name: "TemplateAutostopRequirement1HourSkip",
|
||||||
now: saturdayMidnightSydney.Add(-59 * time.Minute),
|
buildCompletedAt: saturdayMidnightSydney.Add(-59 * time.Minute),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -188,7 +188,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// The next autostop requirement should be skipped if the
|
// The next autostop requirement should be skipped if the
|
||||||
// workspace is started within 1 hour of it.
|
// workspace is started within 1 hour of it.
|
||||||
name: "TemplateAutostopRequirementDaily",
|
name: "TemplateAutostopRequirementDaily",
|
||||||
now: fridayEveningSydney,
|
buildCompletedAt: fridayEveningSydney,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -202,7 +202,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirementFortnightly/Skip",
|
name: "TemplateAutostopRequirementFortnightly/Skip",
|
||||||
now: wednesdayMidnightUTC,
|
buildCompletedAt: wednesdayMidnightUTC,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -216,7 +216,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirementFortnightly/NoSkip",
|
name: "TemplateAutostopRequirementFortnightly/NoSkip",
|
||||||
now: wednesdayMidnightUTC.AddDate(0, 0, 7),
|
buildCompletedAt: wednesdayMidnightUTC.AddDate(0, 0, 7),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -230,7 +230,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirementTriweekly/Skip",
|
name: "TemplateAutostopRequirementTriweekly/Skip",
|
||||||
now: wednesdayMidnightUTC,
|
buildCompletedAt: wednesdayMidnightUTC,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -246,7 +246,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirementTriweekly/NoSkip",
|
name: "TemplateAutostopRequirementTriweekly/NoSkip",
|
||||||
now: wednesdayMidnightUTC.AddDate(0, 0, 7),
|
buildCompletedAt: wednesdayMidnightUTC.AddDate(0, 0, 7),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -262,7 +262,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
name: "TemplateAutostopRequirementOverridesWorkspaceTTL",
|
name: "TemplateAutostopRequirementOverridesWorkspaceTTL",
|
||||||
// now doesn't have to be UTC, but it helps us ensure that
|
// now doesn't have to be UTC, but it helps us ensure that
|
||||||
// timezones are compared correctly in this test.
|
// timezones are compared correctly in this test.
|
||||||
now: fridayEveningSydney.In(time.UTC),
|
buildCompletedAt: fridayEveningSydney.In(time.UTC),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -276,7 +276,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TemplateAutostopRequirementOverridesTemplateDefaultTTL",
|
name: "TemplateAutostopRequirementOverridesTemplateDefaultTTL",
|
||||||
now: fridayEveningSydney.In(time.UTC),
|
buildCompletedAt: fridayEveningSydney.In(time.UTC),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 3 * time.Hour,
|
templateDefaultTTL: 3 * time.Hour,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -293,7 +293,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// The epoch is 2023-01-02 in each timezone. We set the time to
|
// The epoch is 2023-01-02 in each timezone. We set the time to
|
||||||
// 1 second before 11pm the previous day, as this is the latest time
|
// 1 second before 11pm the previous day, as this is the latest time
|
||||||
// we allow due to our 2h leeway logic.
|
// we allow due to our 2h leeway logic.
|
||||||
now: time.Date(2023, 1, 1, 21, 59, 59, 0, sydneyLoc),
|
buildCompletedAt: time.Date(2023, 1, 1, 21, 59, 59, 0, sydneyLoc),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -306,7 +306,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DaylightSavings/OK",
|
name: "DaylightSavings/OK",
|
||||||
now: duringDst,
|
buildCompletedAt: duringDst,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -320,7 +320,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DaylightSavings/SwitchMidWeek/In",
|
name: "DaylightSavings/SwitchMidWeek/In",
|
||||||
now: beforeDstIn,
|
buildCompletedAt: beforeDstIn,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -334,7 +334,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DaylightSavings/SwitchMidWeek/Out",
|
name: "DaylightSavings/SwitchMidWeek/Out",
|
||||||
now: beforeDstOut,
|
buildCompletedAt: beforeDstOut,
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: sydneyQuietHours,
|
userQuietHoursSchedule: sydneyQuietHours,
|
||||||
@@ -348,7 +348,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/In",
|
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/In",
|
||||||
now: beforeDstIn.Add(-24 * time.Hour),
|
buildCompletedAt: beforeDstIn.Add(-24 * time.Hour),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: dstInQuietHours,
|
userQuietHoursSchedule: dstInQuietHours,
|
||||||
@@ -362,7 +362,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/Out",
|
name: "DaylightSavings/QuietHoursFallsOnDstSwitch/Out",
|
||||||
now: beforeDstOut.Add(-24 * time.Hour),
|
buildCompletedAt: beforeDstOut.Add(-24 * time.Hour),
|
||||||
templateAllowAutostop: true,
|
templateAllowAutostop: true,
|
||||||
templateDefaultTTL: 0,
|
templateDefaultTTL: 0,
|
||||||
userQuietHoursSchedule: dstOutQuietHours,
|
userQuietHoursSchedule: dstOutQuietHours,
|
||||||
@@ -382,7 +382,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// activity on the workspace.
|
// activity on the workspace.
|
||||||
name: "AutostopCrossAutostartBorder",
|
name: "AutostopCrossAutostartBorder",
|
||||||
// Starting at 9:45pm, with the autostart at 9am.
|
// Starting at 9:45pm, with the autostart at 9am.
|
||||||
now: pastDateNight,
|
buildCompletedAt: pastDateNight,
|
||||||
templateAllowAutostop: false,
|
templateAllowAutostop: false,
|
||||||
templateDefaultTTL: time.Hour * 12,
|
templateDefaultTTL: time.Hour * 12,
|
||||||
workspaceTTL: time.Hour * 12,
|
workspaceTTL: time.Hour * 12,
|
||||||
@@ -405,7 +405,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// Same as AutostopCrossAutostartBorder, but just misses the autostart.
|
// Same as AutostopCrossAutostartBorder, but just misses the autostart.
|
||||||
name: "AutostopCrossMissAutostartBorder",
|
name: "AutostopCrossMissAutostartBorder",
|
||||||
// Starting at 8:45pm, with the autostart at 9am.
|
// Starting at 8:45pm, with the autostart at 9am.
|
||||||
now: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day(), 20, 30, 0, 0, chicago),
|
buildCompletedAt: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day(), 20, 30, 0, 0, chicago),
|
||||||
templateAllowAutostop: false,
|
templateAllowAutostop: false,
|
||||||
templateDefaultTTL: time.Hour * 12,
|
templateDefaultTTL: time.Hour * 12,
|
||||||
workspaceTTL: time.Hour * 12,
|
workspaceTTL: time.Hour * 12,
|
||||||
@@ -429,7 +429,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// The autostop deadline is before the autostart threshold.
|
// The autostop deadline is before the autostart threshold.
|
||||||
name: "AutostopCrossAutostartBorderMaxEarlyDeadline",
|
name: "AutostopCrossAutostartBorderMaxEarlyDeadline",
|
||||||
// Starting at 9:45pm, with the autostart at 9am.
|
// Starting at 9:45pm, with the autostart at 9am.
|
||||||
now: pastDateNight,
|
buildCompletedAt: pastDateNight,
|
||||||
templateAllowAutostop: false,
|
templateAllowAutostop: false,
|
||||||
templateDefaultTTL: time.Hour * 12,
|
templateDefaultTTL: time.Hour * 12,
|
||||||
workspaceTTL: time.Hour * 12,
|
workspaceTTL: time.Hour * 12,
|
||||||
@@ -459,7 +459,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
// So the deadline is > 12 hours, but stops at the max deadline.
|
// So the deadline is > 12 hours, but stops at the max deadline.
|
||||||
name: "AutostopCrossAutostartBorderMaxDeadline",
|
name: "AutostopCrossAutostartBorderMaxDeadline",
|
||||||
// Starting at 9:45pm, with the autostart at 9am.
|
// Starting at 9:45pm, with the autostart at 9am.
|
||||||
now: pastDateNight,
|
buildCompletedAt: pastDateNight,
|
||||||
templateAllowAutostop: false,
|
templateAllowAutostop: false,
|
||||||
templateDefaultTTL: time.Hour * 12,
|
templateDefaultTTL: time.Hour * 12,
|
||||||
workspaceTTL: time.Hour * 12,
|
workspaceTTL: time.Hour * 12,
|
||||||
@@ -571,7 +571,7 @@ func TestCalculateAutoStop(t *testing.T) {
|
|||||||
Database: db,
|
Database: db,
|
||||||
TemplateScheduleStore: templateScheduleStore,
|
TemplateScheduleStore: templateScheduleStore,
|
||||||
UserQuietHoursScheduleStore: userQuietHoursScheduleStore,
|
UserQuietHoursScheduleStore: userQuietHoursScheduleStore,
|
||||||
Now: c.now,
|
WorkspaceBuildCompletedAt: c.buildCompletedAt,
|
||||||
Workspace: workspace,
|
Workspace: workspace,
|
||||||
WorkspaceAutostart: c.wsAutostart,
|
WorkspaceAutostart: c.wsAutostart,
|
||||||
})
|
})
|
||||||
|
|||||||
+20
-6
@@ -45,8 +45,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ttlMin = time.Minute //nolint:revive // min here means 'minimum' not 'minutes'
|
ttlMinimum = time.Minute
|
||||||
ttlMax = 30 * 24 * time.Hour
|
ttlMaximum = 30 * 24 * time.Hour
|
||||||
|
|
||||||
errTTLMin = xerrors.New("time until shutdown must be at least one minute")
|
errTTLMin = xerrors.New("time until shutdown must be at least one minute")
|
||||||
errTTLMax = xerrors.New("time until shutdown must be less than 30 days")
|
errTTLMax = xerrors.New("time until shutdown must be less than 30 days")
|
||||||
@@ -1190,8 +1190,22 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if build.Transition == database.WorkspaceTransitionStart {
|
if build.Transition == database.WorkspaceTransitionStart {
|
||||||
if err = s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
|
if err = s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
|
||||||
ID: build.ID,
|
ID: build.ID,
|
||||||
Deadline: time.Time{},
|
// Use the max_deadline as the new build deadline. It will
|
||||||
|
// either be zero (our target), or a non-zero value that we
|
||||||
|
// need to abide by anyway due to template policy.
|
||||||
|
//
|
||||||
|
// Previously, we would always set the deadline to zero,
|
||||||
|
// which was incorrect behavior. When max_deadline is
|
||||||
|
// non-zero, deadline must be set to a non-zero value that
|
||||||
|
// is less than max_deadline.
|
||||||
|
//
|
||||||
|
// Disabling TTL autostop (at a workspace or template level)
|
||||||
|
// does not trump the template's autostop requirement.
|
||||||
|
//
|
||||||
|
// Refer to the comments on schedule.CalculateAutostop for
|
||||||
|
// more information.
|
||||||
|
Deadline: build.MaxDeadline,
|
||||||
MaxDeadline: build.MaxDeadline,
|
MaxDeadline: build.MaxDeadline,
|
||||||
UpdatedAt: dbtime.Time(api.Clock.Now()),
|
UpdatedAt: dbtime.Time(api.Clock.Now()),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -2391,11 +2405,11 @@ func validWorkspaceTTLMillis(millis *int64, templateDefault time.Duration) (sql.
|
|||||||
|
|
||||||
dur := time.Duration(*millis) * time.Millisecond
|
dur := time.Duration(*millis) * time.Millisecond
|
||||||
truncated := dur.Truncate(time.Minute)
|
truncated := dur.Truncate(time.Minute)
|
||||||
if truncated < ttlMin {
|
if truncated < ttlMinimum {
|
||||||
return sql.NullInt64{}, errTTLMin
|
return sql.NullInt64{}, errTTLMin
|
||||||
}
|
}
|
||||||
|
|
||||||
if truncated > ttlMax {
|
if truncated > ttlMaximum {
|
||||||
return sql.NullInt64{}, errTTLMax
|
return sql.NullInt64{}, errTTLMax
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2897,6 +2897,55 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveAutostopWithRunningWorkspaceWithMaxDeadline", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = testutil.Context(t, testutil.WaitLong)
|
||||||
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user = coderdtest.CreateFirstUser(t, client)
|
||||||
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
deadline = 8 * time.Hour
|
||||||
|
maxDeadline = 10 * time.Hour
|
||||||
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||||
|
cwr.TTLMillis = ptr.Ref(deadline.Milliseconds())
|
||||||
|
})
|
||||||
|
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a hack, but the max_deadline isn't precisely configurable
|
||||||
|
// without a lot of unnecessary hassle.
|
||||||
|
dbBuild, err := db.GetWorkspaceBuildByID(dbauthz.AsSystemRestricted(ctx), build.ID) //nolint:gocritic // test
|
||||||
|
require.NoError(t, err)
|
||||||
|
dbJob, err := db.GetProvisionerJobByID(dbauthz.AsSystemRestricted(ctx), dbBuild.JobID) //nolint:gocritic // test
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, dbJob.CompletedAt.Valid)
|
||||||
|
expectedMaxDeadline := dbJob.CompletedAt.Time.Add(maxDeadline)
|
||||||
|
err = db.UpdateWorkspaceBuildDeadlineByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceBuildDeadlineByIDParams{ //nolint:gocritic // test
|
||||||
|
ID: build.ID,
|
||||||
|
Deadline: dbBuild.Deadline,
|
||||||
|
MaxDeadline: expectedMaxDeadline,
|
||||||
|
UpdatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Remove autostop.
|
||||||
|
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
||||||
|
TTLMillis: nil,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Expect that the deadline is set to the max_deadline.
|
||||||
|
build, err = client.WorkspaceBuild(ctx, build.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, build.Deadline.Valid)
|
||||||
|
require.WithinDuration(t, build.Deadline.Time, expectedMaxDeadline, time.Second)
|
||||||
|
require.True(t, build.MaxDeadline.Valid)
|
||||||
|
require.WithinDuration(t, build.MaxDeadline.Time, expectedMaxDeadline, time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("CustomAutostopDisabledByTemplate", func(t *testing.T) {
|
t.Run("CustomAutostopDisabledByTemplate", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -350,14 +350,23 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the new autostop max_deadline from the workspace. Since
|
||||||
|
// autostop is always calculated from the build completion time, we don't
|
||||||
|
// want to use the returned autostop.Deadline property as it will likely be
|
||||||
|
// in the distant past.
|
||||||
|
//
|
||||||
|
// The only exception is if the newly calculated workspace TTL is now zero,
|
||||||
|
// which means the workspace can now stay on indefinitely.
|
||||||
|
//
|
||||||
|
// This also matches the behavior of updating a workspace's TTL, where we
|
||||||
|
// don't apply the changes until the workspace is rebuilt.
|
||||||
autostop, err := agpl.CalculateAutostop(ctx, agpl.CalculateAutostopParams{
|
autostop, err := agpl.CalculateAutostop(ctx, agpl.CalculateAutostopParams{
|
||||||
Database: db,
|
Database: db,
|
||||||
TemplateScheduleStore: s,
|
TemplateScheduleStore: s,
|
||||||
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
||||||
// Use the job completion time as the time we calculate autostop from.
|
WorkspaceBuildCompletedAt: job.CompletedAt.Time,
|
||||||
Now: job.CompletedAt.Time,
|
Workspace: workspace.WorkspaceTable(),
|
||||||
Workspace: workspace.WorkspaceTable(),
|
WorkspaceAutostart: workspace.AutostartSchedule.String,
|
||||||
WorkspaceAutostart: workspace.AutostartSchedule.String,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("calculate new autostop for workspace %q: %w", workspace.ID, err)
|
return xerrors.Errorf("calculate new autostop for workspace %q: %w", workspace.ID, err)
|
||||||
@@ -389,9 +398,24 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
|
|||||||
autostop.MaxDeadline = now.Add(time.Hour * 2)
|
autostop.MaxDeadline = now.Add(time.Hour * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the new deadline is zero, the workspace can now stay on indefinitely.
|
||||||
|
// Otherwise, we want to discard the new value as per the comment above the
|
||||||
|
// CalculateAutostop call.
|
||||||
|
//
|
||||||
|
// We could potentially calculate a new deadline based on the TTL setting
|
||||||
|
// (on either the workspace or the template based on the template's policy)
|
||||||
|
// against the current time, but doing nothing here matches the current
|
||||||
|
// behavior of the workspace TTL update endpoint.
|
||||||
|
//
|
||||||
|
// Per the documentation of CalculateAutostop, the deadline is not intended
|
||||||
|
// as a policy measure, so it's fine that we don't update it when the
|
||||||
|
// template schedule changes.
|
||||||
|
if !autostop.Deadline.IsZero() {
|
||||||
|
autostop.Deadline = build.Deadline
|
||||||
|
}
|
||||||
|
|
||||||
// If the current deadline on the build is after the new max_deadline, then
|
// If the current deadline on the build is after the new max_deadline, then
|
||||||
// set it to the max_deadline.
|
// set it to the max_deadline.
|
||||||
autostop.Deadline = build.Deadline
|
|
||||||
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.After(autostop.MaxDeadline) {
|
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.After(autostop.MaxDeadline) {
|
||||||
autostop.Deadline = autostop.MaxDeadline
|
autostop.Deadline = autostop.MaxDeadline
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/notifications"
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
||||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
||||||
"github.com/coder/coder/v2/cryptorand"
|
"github.com/coder/coder/v2/cryptorand"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
@@ -73,17 +72,23 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
buildTime := time.Date(nowY, nowM, nowD, 12, 0, 0, 0, time.UTC) // noon today UTC
|
buildTime := time.Date(nowY, nowM, nowD, 12, 0, 0, 0, time.UTC) // noon today UTC
|
||||||
nextQuietHours := time.Date(nowY, nowM, nowD+1, 0, 0, 0, 0, time.UTC) // midnight tomorrow UTC
|
nextQuietHours := time.Date(nowY, nowM, nowD+1, 0, 0, 0, 0, time.UTC) // midnight tomorrow UTC
|
||||||
|
|
||||||
// Workspace old max_deadline too soon
|
defaultTTL := 8 * time.Hour
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
now time.Time
|
now time.Time
|
||||||
|
// Before:
|
||||||
deadline time.Time
|
deadline time.Time
|
||||||
maxDeadline time.Time
|
maxDeadline time.Time
|
||||||
// Set to nil for no change.
|
// After:
|
||||||
newDeadline *time.Time
|
newDeadline time.Time
|
||||||
newMaxDeadline time.Time
|
newMaxDeadline time.Time
|
||||||
noQuietHours bool
|
// Config:
|
||||||
autostopReq *agplschedule.TemplateAutostopRequirement
|
noQuietHours bool
|
||||||
|
// Note that ttl will not influence the new build at all unless it's 0
|
||||||
|
// AND the build does not have a max deadline post recalculation.
|
||||||
|
ttl time.Duration
|
||||||
|
autostopReq *agplschedule.TemplateAutostopRequirement
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "SkippedWorkspaceMaxDeadlineTooSoon",
|
name: "SkippedWorkspaceMaxDeadlineTooSoon",
|
||||||
@@ -91,8 +96,9 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
maxDeadline: buildTime.Add(1 * time.Hour),
|
maxDeadline: buildTime.Add(1 * time.Hour),
|
||||||
// Unchanged since the max deadline is too soon.
|
// Unchanged since the max deadline is too soon.
|
||||||
newDeadline: nil,
|
newDeadline: buildTime,
|
||||||
newMaxDeadline: buildTime.Add(1 * time.Hour),
|
newMaxDeadline: buildTime.Add(1 * time.Hour),
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NewWorkspaceMaxDeadlineBeforeNow",
|
name: "NewWorkspaceMaxDeadlineBeforeNow",
|
||||||
@@ -101,10 +107,11 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: nil,
|
newDeadline: buildTime,
|
||||||
// We will use now() + 2 hours if the newly calculated max deadline
|
// We will use now() + 2 hours if the newly calculated max deadline
|
||||||
// from the workspace build time is before now.
|
// from the workspace build time is before now.
|
||||||
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
|
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NewWorkspaceMaxDeadlineSoon",
|
name: "NewWorkspaceMaxDeadlineSoon",
|
||||||
@@ -113,10 +120,11 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: nil,
|
newDeadline: buildTime,
|
||||||
// We will use now() + 2 hours if the newly calculated max deadline
|
// We will use now() + 2 hours if the newly calculated max deadline
|
||||||
// from the workspace build time is within the next 2 hours.
|
// from the workspace build time is within the next 2 hours.
|
||||||
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
|
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NewWorkspaceMaxDeadlineFuture",
|
name: "NewWorkspaceMaxDeadlineFuture",
|
||||||
@@ -125,8 +133,9 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: nil,
|
newDeadline: buildTime,
|
||||||
newMaxDeadline: nextQuietHours,
|
newMaxDeadline: nextQuietHours,
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DeadlineAfterNewWorkspaceMaxDeadline",
|
name: "DeadlineAfterNewWorkspaceMaxDeadline",
|
||||||
@@ -136,8 +145,9 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: nextQuietHours.Add(24 * time.Hour),
|
deadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
// The deadline should match since it is after the new max deadline.
|
// The deadline should match since it is after the new max deadline.
|
||||||
newDeadline: ptr.Ref(nextQuietHours),
|
newDeadline: nextQuietHours,
|
||||||
newMaxDeadline: nextQuietHours,
|
newMaxDeadline: nextQuietHours,
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// There was a bug if a user has no quiet hours set, and autostop
|
// There was a bug if a user has no quiet hours set, and autostop
|
||||||
@@ -151,13 +161,14 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime.Add(time.Hour * 8),
|
deadline: buildTime.Add(time.Hour * 8),
|
||||||
maxDeadline: time.Time{}, // No max set
|
maxDeadline: time.Time{}, // No max set
|
||||||
// Should be unchanged
|
// Should be unchanged
|
||||||
newDeadline: ptr.Ref(buildTime.Add(time.Hour * 8)),
|
newDeadline: buildTime.Add(time.Hour * 8),
|
||||||
newMaxDeadline: time.Time{},
|
newMaxDeadline: time.Time{},
|
||||||
noQuietHours: true,
|
noQuietHours: true,
|
||||||
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: 0,
|
DaysOfWeek: 0,
|
||||||
Weeks: 0,
|
Weeks: 0,
|
||||||
},
|
},
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// A bug existed where MaxDeadline could be set, but deadline was
|
// A bug existed where MaxDeadline could be set, but deadline was
|
||||||
@@ -168,15 +179,15 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: time.Time{},
|
deadline: time.Time{},
|
||||||
maxDeadline: time.Time{}, // No max set
|
maxDeadline: time.Time{}, // No max set
|
||||||
// Should be unchanged
|
// Should be unchanged
|
||||||
newDeadline: ptr.Ref(time.Time{}),
|
newDeadline: time.Time{},
|
||||||
newMaxDeadline: time.Time{},
|
newMaxDeadline: time.Time{},
|
||||||
noQuietHours: true,
|
noQuietHours: true,
|
||||||
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: 0,
|
DaysOfWeek: 0,
|
||||||
Weeks: 0,
|
Weeks: 0,
|
||||||
},
|
},
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
// Similar to 'NoDeadline' test. This has a MaxDeadline set, so
|
// Similar to 'NoDeadline' test. This has a MaxDeadline set, so
|
||||||
// the deadline of the workspace should now be set.
|
// the deadline of the workspace should now be set.
|
||||||
@@ -185,8 +196,26 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
// Start with unset times
|
// Start with unset times
|
||||||
deadline: time.Time{},
|
deadline: time.Time{},
|
||||||
maxDeadline: time.Time{},
|
maxDeadline: time.Time{},
|
||||||
newDeadline: ptr.Ref(nextQuietHours),
|
newDeadline: nextQuietHours,
|
||||||
newMaxDeadline: nextQuietHours,
|
newMaxDeadline: nextQuietHours,
|
||||||
|
ttl: defaultTTL, // no effect
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If the build doesn't have a max_deadline anymore, and there is no
|
||||||
|
// TTL anymore, then both the deadline and max_deadline should be
|
||||||
|
// zero.
|
||||||
|
name: "NoTTLNoDeadlineNoMaxDeadline",
|
||||||
|
now: buildTime,
|
||||||
|
deadline: buildTime.Add(time.Hour * 8),
|
||||||
|
maxDeadline: buildTime.Add(time.Hour * 8),
|
||||||
|
newDeadline: time.Time{},
|
||||||
|
newMaxDeadline: time.Time{},
|
||||||
|
noQuietHours: true,
|
||||||
|
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
||||||
|
DaysOfWeek: 0,
|
||||||
|
Weeks: 0,
|
||||||
|
},
|
||||||
|
ttl: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +235,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
t.Log("maxDeadline", c.maxDeadline)
|
t.Log("maxDeadline", c.maxDeadline)
|
||||||
t.Log("newDeadline", c.newDeadline)
|
t.Log("newDeadline", c.newDeadline)
|
||||||
t.Log("newMaxDeadline", c.newMaxDeadline)
|
t.Log("newMaxDeadline", c.newMaxDeadline)
|
||||||
|
t.Log("ttl", c.ttl)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
template = dbgen.Template(t, db, database.Template{
|
template = dbgen.Template(t, db, database.Template{
|
||||||
@@ -300,7 +330,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
|
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
|
||||||
UserAutostartEnabled: false,
|
UserAutostartEnabled: false,
|
||||||
UserAutostopEnabled: false,
|
UserAutostopEnabled: false,
|
||||||
DefaultTTL: 0,
|
DefaultTTL: c.ttl,
|
||||||
AutostopRequirement: autostopReq,
|
AutostopRequirement: autostopReq,
|
||||||
FailureTTL: 0,
|
FailureTTL: 0,
|
||||||
TimeTilDormant: 0,
|
TimeTilDormant: 0,
|
||||||
@@ -312,11 +342,8 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
|
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if c.newDeadline == nil {
|
require.WithinDuration(t, c.newDeadline, newBuild.Deadline, time.Second, "deadline")
|
||||||
c.newDeadline = &wsBuild.Deadline
|
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second, "max_deadline")
|
||||||
}
|
|
||||||
require.WithinDuration(t, *c.newDeadline, newBuild.Deadline, time.Second)
|
|
||||||
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
|
|
||||||
|
|
||||||
// Check that the new build has the same state as before.
|
// Check that the new build has the same state as before.
|
||||||
require.Equal(t, wsBuild.ProvisionerState, newBuild.ProvisionerState, "provisioner state mismatch")
|
require.Equal(t, wsBuild.ProvisionerState, newBuild.ProvisionerState, "provisioner state mismatch")
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/imports"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type constraintType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
constraintTypeUnique constraintType = "unique"
|
||||||
|
constraintTypeForeignKey constraintType = "foreign_key"
|
||||||
|
constraintTypeCheck constraintType = "check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c constraintType) goType() string {
|
||||||
|
switch c {
|
||||||
|
case constraintTypeUnique:
|
||||||
|
return "UniqueConstraint"
|
||||||
|
case constraintTypeForeignKey:
|
||||||
|
return "ForeignKeyConstraint"
|
||||||
|
case constraintTypeCheck:
|
||||||
|
return "CheckConstraint"
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown constraint type: %s", c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c constraintType) goTypeDescriptionPart() string {
|
||||||
|
switch c {
|
||||||
|
case constraintTypeUnique:
|
||||||
|
return "unique"
|
||||||
|
case constraintTypeForeignKey:
|
||||||
|
return "foreign key"
|
||||||
|
case constraintTypeCheck:
|
||||||
|
return "check"
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown constraint type: %s", c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c constraintType) goEnumNamePrefix() string {
|
||||||
|
switch c {
|
||||||
|
case constraintTypeUnique:
|
||||||
|
return "Unique"
|
||||||
|
case constraintTypeForeignKey:
|
||||||
|
return "ForeignKey"
|
||||||
|
case constraintTypeCheck:
|
||||||
|
return "Check"
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown constraint type: %s", c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type constraint struct {
|
||||||
|
name string
|
||||||
|
// comment is typically the full constraint, but for check constraints it's
|
||||||
|
// instead the table name.
|
||||||
|
comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryToConstraintsFn is a function that takes a query and returns zero or
|
||||||
|
// more constraints if the query matches the wanted constraint type. If the
|
||||||
|
// query does not match the wanted constraint type, the function should return
|
||||||
|
// no constraints.
|
||||||
|
type queryToConstraintsFn func(query string) ([]constraint, error)
|
||||||
|
|
||||||
|
// generateConstraints does the following:
|
||||||
|
// 1. Read the dump.sql file
|
||||||
|
// 2. Parse the file into each query
|
||||||
|
// 3. Pass each query to the constraintFn function
|
||||||
|
// 4. Generate the enum from the returned constraints
|
||||||
|
// 5. Write the generated code to the output path
|
||||||
|
func generateConstraints(dumpPath, outputPath string, outputConstraintType constraintType, fn queryToConstraintsFn) error {
|
||||||
|
dump, err := os.Open(dumpPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dump.Close()
|
||||||
|
|
||||||
|
var allConstraints []constraint
|
||||||
|
|
||||||
|
dumpScanner := bufio.NewScanner(dump)
|
||||||
|
query := ""
|
||||||
|
for dumpScanner.Scan() {
|
||||||
|
line := strings.TrimSpace(dumpScanner.Text())
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "--"):
|
||||||
|
case line == "":
|
||||||
|
case strings.HasSuffix(line, ";"):
|
||||||
|
query += line
|
||||||
|
newConstraints, err := fn(query)
|
||||||
|
query = ""
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("process query %q: %w", query, err)
|
||||||
|
}
|
||||||
|
allConstraints = append(allConstraints, newConstraints...)
|
||||||
|
default:
|
||||||
|
query += line + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = dumpScanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &bytes.Buffer{}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(s, `// Code generated by scripts/dbgen/main.go. DO NOT EDIT.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// %[1]s represents a named %[2]s constraint on a table.
|
||||||
|
type %[1]s string
|
||||||
|
|
||||||
|
// %[1]s enums.
|
||||||
|
const (
|
||||||
|
`, outputConstraintType.goType(), outputConstraintType.goTypeDescriptionPart())
|
||||||
|
|
||||||
|
for _, c := range allConstraints {
|
||||||
|
constName := outputConstraintType.goEnumNamePrefix() + nameFromSnakeCase(c.name)
|
||||||
|
_, _ = fmt.Fprintf(s, "\t%[1]s %[2]s = %[3]q // %[4]s\n", constName, outputConstraintType.goType(), c.name, c.comment)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(s, ")\n")
|
||||||
|
|
||||||
|
data, err := imports.Process(outputPath, s.Bytes(), &imports.Options{
|
||||||
|
Comments: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(outputPath, data, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateUniqueConstraints generates the UniqueConstraint enum.
|
||||||
|
func generateUniqueConstraints() error {
|
||||||
|
localPath, err := localFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
databasePath := filepath.Join(localPath, "..", "..", "..", "coderd", "database")
|
||||||
|
dumpPath := filepath.Join(databasePath, "dump.sql")
|
||||||
|
outputPath := filepath.Join(databasePath, "unique_constraint.go")
|
||||||
|
|
||||||
|
fn := func(query string) ([]constraint, error) {
|
||||||
|
if strings.Contains(query, "UNIQUE") || strings.Contains(query, "PRIMARY KEY") {
|
||||||
|
name := ""
|
||||||
|
switch {
|
||||||
|
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
|
||||||
|
name = strings.Split(query, " ")[6]
|
||||||
|
case strings.Contains(query, "CREATE UNIQUE INDEX"):
|
||||||
|
name = strings.Split(query, " ")[3]
|
||||||
|
default:
|
||||||
|
return nil, xerrors.Errorf("unknown unique constraint format: %s", query)
|
||||||
|
}
|
||||||
|
return []constraint{
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
comment: query,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return generateConstraints(dumpPath, outputPath, constraintTypeUnique, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateForeignKeyConstraints generates the ForeignKeyConstraint enum.
|
||||||
|
func generateForeignKeyConstraints() error {
|
||||||
|
localPath, err := localFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
databasePath := filepath.Join(localPath, "..", "..", "..", "coderd", "database")
|
||||||
|
dumpPath := filepath.Join(databasePath, "dump.sql")
|
||||||
|
outputPath := filepath.Join(databasePath, "foreign_key_constraint.go")
|
||||||
|
|
||||||
|
fn := func(query string) ([]constraint, error) {
|
||||||
|
if strings.Contains(query, "FOREIGN KEY") {
|
||||||
|
name := ""
|
||||||
|
switch {
|
||||||
|
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
|
||||||
|
name = strings.Split(query, " ")[6]
|
||||||
|
default:
|
||||||
|
return nil, xerrors.Errorf("unknown foreign key constraint format: %s", query)
|
||||||
|
}
|
||||||
|
return []constraint{
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
comment: query,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return []constraint{}, nil
|
||||||
|
}
|
||||||
|
return generateConstraints(dumpPath, outputPath, constraintTypeForeignKey, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCheckConstraints generates the CheckConstraint enum.
|
||||||
|
func generateCheckConstraints() error {
|
||||||
|
localPath, err := localFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
databasePath := filepath.Join(localPath, "..", "..", "..", "coderd", "database")
|
||||||
|
dumpPath := filepath.Join(databasePath, "dump.sql")
|
||||||
|
outputPath := filepath.Join(databasePath, "check_constraint.go")
|
||||||
|
|
||||||
|
var (
|
||||||
|
tableRegex = regexp.MustCompile(`CREATE TABLE\s+([^\s]+)`)
|
||||||
|
checkRegex = regexp.MustCompile(`CONSTRAINT\s+([^\s]+)\s+CHECK`)
|
||||||
|
)
|
||||||
|
fn := func(query string) ([]constraint, error) {
|
||||||
|
constraints := []constraint{}
|
||||||
|
|
||||||
|
tableMatches := tableRegex.FindStringSubmatch(query)
|
||||||
|
if len(tableMatches) > 0 {
|
||||||
|
table := tableMatches[1]
|
||||||
|
|
||||||
|
// Find every CONSTRAINT xxx CHECK occurrence.
|
||||||
|
matches := checkRegex.FindAllStringSubmatch(query, -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
constraints = append(constraints, constraint{
|
||||||
|
name: match[1],
|
||||||
|
comment: table,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return constraints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateConstraints(dumpPath, outputPath, constraintTypeCheck, fn)
|
||||||
|
}
|
||||||
+5
-146
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
@@ -80,154 +79,14 @@ return %s
|
|||||||
return xerrors.Errorf("generate foreign key constraints: %w", err)
|
return xerrors.Errorf("generate foreign key constraints: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = generateCheckConstraints()
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("generate check constraints: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateUniqueConstraints generates the UniqueConstraint enum.
|
|
||||||
func generateUniqueConstraints() error {
|
|
||||||
localPath, err := localFilePath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
databasePath := filepath.Join(localPath, "..", "..", "..", "coderd", "database")
|
|
||||||
|
|
||||||
dump, err := os.Open(filepath.Join(databasePath, "dump.sql"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dump.Close()
|
|
||||||
|
|
||||||
var uniqueConstraints []string
|
|
||||||
dumpScanner := bufio.NewScanner(dump)
|
|
||||||
query := ""
|
|
||||||
for dumpScanner.Scan() {
|
|
||||||
line := strings.TrimSpace(dumpScanner.Text())
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(line, "--"):
|
|
||||||
case line == "":
|
|
||||||
case strings.HasSuffix(line, ";"):
|
|
||||||
query += line
|
|
||||||
if strings.Contains(query, "UNIQUE") || strings.Contains(query, "PRIMARY KEY") {
|
|
||||||
uniqueConstraints = append(uniqueConstraints, query)
|
|
||||||
}
|
|
||||||
query = ""
|
|
||||||
default:
|
|
||||||
query += line + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = dumpScanner.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &bytes.Buffer{}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(s, `// Code generated by scripts/dbgen/main.go. DO NOT EDIT.
|
|
||||||
package database
|
|
||||||
`)
|
|
||||||
_, _ = fmt.Fprint(s, `
|
|
||||||
// UniqueConstraint represents a named unique constraint on a table.
|
|
||||||
type UniqueConstraint string
|
|
||||||
|
|
||||||
// UniqueConstraint enums.
|
|
||||||
const (
|
|
||||||
`)
|
|
||||||
for _, query := range uniqueConstraints {
|
|
||||||
name := ""
|
|
||||||
switch {
|
|
||||||
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
|
|
||||||
name = strings.Split(query, " ")[6]
|
|
||||||
case strings.Contains(query, "CREATE UNIQUE INDEX"):
|
|
||||||
name = strings.Split(query, " ")[3]
|
|
||||||
default:
|
|
||||||
return xerrors.Errorf("unknown unique constraint format: %s", query)
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintf(s, "\tUnique%s UniqueConstraint = %q // %s\n", nameFromSnakeCase(name), name, query)
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprint(s, ")\n")
|
|
||||||
|
|
||||||
outputPath := filepath.Join(databasePath, "unique_constraint.go")
|
|
||||||
|
|
||||||
data, err := imports.Process(outputPath, s.Bytes(), &imports.Options{
|
|
||||||
Comments: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(outputPath, data, 0o600)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateForeignKeyConstraints generates the ForeignKeyConstraint enum.
|
|
||||||
func generateForeignKeyConstraints() error {
|
|
||||||
localPath, err := localFilePath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
databasePath := filepath.Join(localPath, "..", "..", "..", "coderd", "database")
|
|
||||||
|
|
||||||
dump, err := os.Open(filepath.Join(databasePath, "dump.sql"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dump.Close()
|
|
||||||
|
|
||||||
var foreignKeyConstraints []string
|
|
||||||
dumpScanner := bufio.NewScanner(dump)
|
|
||||||
query := ""
|
|
||||||
for dumpScanner.Scan() {
|
|
||||||
line := strings.TrimSpace(dumpScanner.Text())
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(line, "--"):
|
|
||||||
case line == "":
|
|
||||||
case strings.HasSuffix(line, ";"):
|
|
||||||
query += line
|
|
||||||
if strings.Contains(query, "FOREIGN KEY") {
|
|
||||||
foreignKeyConstraints = append(foreignKeyConstraints, query)
|
|
||||||
}
|
|
||||||
query = ""
|
|
||||||
default:
|
|
||||||
query += line + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dumpScanner.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &bytes.Buffer{}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(s, `// Code generated by scripts/dbgen/main.go. DO NOT EDIT.
|
|
||||||
package database
|
|
||||||
`)
|
|
||||||
_, _ = fmt.Fprint(s, `
|
|
||||||
// ForeignKeyConstraint represents a named foreign key constraint on a table.
|
|
||||||
type ForeignKeyConstraint string
|
|
||||||
|
|
||||||
// ForeignKeyConstraint enums.
|
|
||||||
const (
|
|
||||||
`)
|
|
||||||
for _, query := range foreignKeyConstraints {
|
|
||||||
name := ""
|
|
||||||
switch {
|
|
||||||
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
|
|
||||||
name = strings.Split(query, " ")[6]
|
|
||||||
default:
|
|
||||||
return xerrors.Errorf("unknown foreign key constraint format: %s", query)
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintf(s, "\tForeignKey%s ForeignKeyConstraint = %q // %s\n", nameFromSnakeCase(name), name, query)
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprint(s, ")\n")
|
|
||||||
|
|
||||||
outputPath := filepath.Join(databasePath, "foreign_key_constraint.go")
|
|
||||||
|
|
||||||
data, err := imports.Process(outputPath, s.Bytes(), &imports.Options{
|
|
||||||
Comments: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(outputPath, data, 0o600)
|
|
||||||
}
|
|
||||||
|
|
||||||
type stubParams struct {
|
type stubParams struct {
|
||||||
FuncName string
|
FuncName string
|
||||||
Parameters string
|
Parameters string
|
||||||
|
|||||||
Reference in New Issue
Block a user