feat: update workspace deadline when template policy changes (#8964)

This commit is contained in:
Dean Sheather
2023-08-14 14:16:47 -07:00
committed by GitHub
parent 37f9d4b783
commit 47b8bf6585
14 changed files with 871 additions and 22 deletions

View File

@@ -6,10 +6,16 @@ import (
"time"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/database/dbauthz"
agpl "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/codersdk"
)
// EnterpriseTemplateScheduleStore provides an agpl.TemplateScheduleStore that
@@ -20,16 +26,35 @@ type EnterpriseTemplateScheduleStore struct {
// workspace build. This value is determined by a feature flag, licensing,
// and whether a default user quiet hours schedule is set.
UseRestartRequirement atomic.Bool
// UserQuietHoursScheduleStore is used when recalculating build deadlines on
// update.
UserQuietHoursScheduleStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore]
// Custom time.Now() function to use in tests. Defaults to database.Now().
TimeNowFn func() time.Time
}
var _ agpl.TemplateScheduleStore = &EnterpriseTemplateScheduleStore{}
func NewEnterpriseTemplateScheduleStore() *EnterpriseTemplateScheduleStore {
return &EnterpriseTemplateScheduleStore{}
func NewEnterpriseTemplateScheduleStore(userQuietHoursStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore]) *EnterpriseTemplateScheduleStore {
return &EnterpriseTemplateScheduleStore{
UserQuietHoursScheduleStore: userQuietHoursStore,
}
}
func (s *EnterpriseTemplateScheduleStore) now() time.Time {
if s.TimeNowFn != nil {
return s.TimeNowFn()
}
return database.Now()
}
// Get implements agpl.TemplateScheduleStore.
func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (agpl.TemplateScheduleOptions, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
return agpl.TemplateScheduleOptions{}, err
@@ -65,7 +90,10 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S
}
// Set implements agpl.TemplateScheduleStore.
func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts agpl.TemplateScheduleOptions) (database.Template, error) {
func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts agpl.TemplateScheduleOptions) (database.Template, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
int64(opts.MaxTTL) == tpl.MaxTTL &&
int16(opts.RestartRequirement.DaysOfWeek) == tpl.RestartRequirementDaysOfWeek &&
@@ -86,9 +114,12 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
var template database.Template
err = db.InTx(func(db database.Store) error {
ctx, span := tracing.StartSpanWithName(ctx, "(*schedule.EnterpriseTemplateScheduleStore).Set()-InTx()")
defer span.End()
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: tpl.ID,
UpdatedAt: database.Now(),
UpdatedAt: s.now(),
AllowUserAutostart: opts.UserAutostartEnabled,
AllowUserAutostop: opts.UserAutostopEnabled,
DefaultTTL: int64(opts.DefaultTTL),
@@ -115,12 +146,20 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
return xerrors.Errorf("update deleting_at of all workspaces for new locked_ttl %q: %w", opts.LockedTTL, err)
}
// TODO: update all workspace max_deadlines to be within new bounds
template, err = db.GetTemplateByID(ctx, tpl.ID)
if err != nil {
return xerrors.Errorf("get updated template schedule: %w", err)
}
// Recalculate max_deadline and deadline for all running workspace
// builds on this template.
if s.UseRestartRequirement.Load() {
err = s.updateWorkspaceBuilds(ctx, db, template)
if err != nil {
return xerrors.Errorf("update workspace builds: %w", err)
}
}
return nil
}, nil)
if err != nil {
@@ -129,3 +168,98 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
return template, nil
}
func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuilds(ctx context.Context, db database.Store, template database.Template) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
//nolint:gocritic // This function will retrieve all workspace builds on
// the template and update their max deadline to be within the new
// policy parameters.
ctx = dbauthz.AsSystemRestricted(ctx)
builds, err := db.GetActiveWorkspaceBuildsByTemplateID(ctx, template.ID)
if err != nil {
return xerrors.Errorf("get active workspace builds: %w", err)
}
for _, build := range builds {
err := s.updateWorkspaceBuild(ctx, db, build)
if err != nil {
return xerrors.Errorf("update workspace build %q: %w", build.ID, err)
}
}
return nil
}
func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Context, db database.Store, build database.WorkspaceBuild) error {
ctx, span := tracing.StartSpan(ctx,
trace.WithAttributes(attribute.String("coder.workspace_id", build.WorkspaceID.String())),
trace.WithAttributes(attribute.String("coder.workspace_build_id", build.ID.String())),
)
defer span.End()
if !build.MaxDeadline.IsZero() && build.MaxDeadline.Before(s.now().Add(2*time.Hour)) {
// Skip this since it's already too close to the max_deadline.
return nil
}
workspace, err := db.GetWorkspaceByID(ctx, build.WorkspaceID)
if err != nil {
return xerrors.Errorf("get workspace %q: %w", build.WorkspaceID, err)
}
job, err := db.GetProvisionerJobByID(ctx, build.JobID)
if err != nil {
return xerrors.Errorf("get provisioner job %q: %w", build.JobID, err)
}
if db2sdk.ProvisionerJobStatus(job) != codersdk.ProvisionerJobSucceeded {
// Only touch builds that are completed.
return nil
}
// If the job completed before the autostop epoch, then it must be skipped
// to avoid failures below. Add a week to account for timezones.
if job.CompletedAt.Time.Before(agpl.TemplateRestartRequirementEpoch(time.UTC).Add(time.Hour * 7 * 24)) {
return nil
}
autostop, err := agpl.CalculateAutostop(ctx, agpl.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: s,
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
// Use the job completion time as the time we calculate autostop from.
Now: job.CompletedAt.Time,
Workspace: workspace,
})
if err != nil {
return xerrors.Errorf("calculate new autostop for workspace %q: %w", workspace.ID, err)
}
// If max deadline is before now()+2h, then set it to that.
now := s.now()
if autostop.MaxDeadline.Before(now.Add(2 * time.Hour)) {
autostop.MaxDeadline = now.Add(time.Hour * 2)
}
// If the current deadline on the build is after the new max_deadline, then
// set it to the max_deadline.
autostop.Deadline = build.Deadline
if autostop.Deadline.After(autostop.MaxDeadline) {
autostop.Deadline = autostop.MaxDeadline
}
// Update the workspace build.
err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: now,
Deadline: autostop.Deadline,
MaxDeadline: autostop.MaxDeadline,
})
if err != nil {
return xerrors.Errorf("update workspace build %q: %w", build.ID, err)
}
return nil
}

View File

@@ -0,0 +1,523 @@
package schedule_test
import (
"database/sql"
"encoding/json"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbgen"
"github.com/coder/coder/coderd/database/dbtestutil"
agplschedule "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/enterprise/coderd/schedule"
"github.com/coder/coder/testutil"
)
func TestTemplateUpdateBuildDeadlines(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
var (
org = dbgen.Organization(t, db, database.Organization{})
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{
CreatedBy: user.ID,
})
templateJob = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
OrganizationID: org.ID,
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{
"foo": "bar",
},
})
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: templateJob.ID,
})
)
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
ctx := testutil.Context(t, testutil.WaitLong)
user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
ID: user.ID,
QuietHoursSchedule: userQuietHoursSchedule,
})
require.NoError(t, err)
realNow := time.Now().UTC()
nowY, nowM, nowD := realNow.Date()
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
// Workspace old max_deadline too soon
cases := []struct {
name string
now time.Time
deadline time.Time
maxDeadline time.Time
newDeadline time.Time // 0 for no change
newMaxDeadline time.Time
}{
{
name: "SkippedWorkspaceMaxDeadlineTooSoon",
now: buildTime,
deadline: buildTime,
maxDeadline: buildTime.Add(1 * time.Hour),
// Unchanged since the max deadline is too soon.
newDeadline: time.Time{},
newMaxDeadline: buildTime.Add(1 * time.Hour),
},
{
name: "NewWorkspaceMaxDeadlineBeforeNow",
// After the new max deadline...
now: nextQuietHours.Add(6 * time.Hour),
deadline: buildTime,
// Far into the future...
maxDeadline: nextQuietHours.Add(24 * time.Hour),
newDeadline: time.Time{},
// We will use now() + 2 hours if the newly calculated max deadline
// from the workspace build time is before now.
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
},
{
name: "NewWorkspaceMaxDeadlineSoon",
// Right before the new max deadline...
now: nextQuietHours.Add(-1 * time.Hour),
deadline: buildTime,
// Far into the future...
maxDeadline: nextQuietHours.Add(24 * time.Hour),
newDeadline: time.Time{},
// We will use now() + 2 hours if the newly calculated max deadline
// from the workspace build time is within the next 2 hours.
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
},
{
name: "NewWorkspaceMaxDeadlineFuture",
// Well before the new max deadline...
now: nextQuietHours.Add(-6 * time.Hour),
deadline: buildTime,
// Far into the future...
maxDeadline: nextQuietHours.Add(24 * time.Hour),
newDeadline: time.Time{},
newMaxDeadline: nextQuietHours,
},
{
name: "DeadlineAfterNewWorkspaceMaxDeadline",
// Well before the new max deadline...
now: nextQuietHours.Add(-6 * time.Hour),
// Far into the future...
deadline: nextQuietHours.Add(24 * time.Hour),
maxDeadline: nextQuietHours.Add(24 * time.Hour),
// The deadline should match since it is after the new max deadline.
newDeadline: nextQuietHours,
newMaxDeadline: nextQuietHours,
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
t.Log("buildTime", buildTime)
t.Log("nextQuietHours", nextQuietHours)
t.Log("now", c.now)
t.Log("deadline", c.deadline)
t.Log("maxDeadline", c.maxDeadline)
t.Log("newDeadline", c.newDeadline)
t.Log("newMaxDeadline", c.newMaxDeadline)
var (
template = dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
})
ws = dbgen.Workspace(t, db, database.Workspace{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
job = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
OrganizationID: org.ID,
FileID: file.ID,
InitiatorID: user.ID,
Provisioner: database.ProvisionerTypeEcho,
Tags: database.StringMap{
c.name: "yeah",
},
})
wsBuild = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID,
BuildNumber: 1,
JobID: job.ID,
InitiatorID: user.ID,
TemplateVersionID: templateVersion.ID,
})
)
acquiredJob, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
StartedAt: sql.NullTime{
Time: buildTime,
Valid: true,
},
WorkerID: uuid.NullUUID{
UUID: uuid.New(),
Valid: true,
},
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, c.name)),
})
require.NoError(t, err)
require.Equal(t, job.ID, acquiredJob.ID)
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: job.ID,
CompletedAt: sql.NullTime{
Time: buildTime,
Valid: true,
},
UpdatedAt: buildTime,
})
require.NoError(t, err)
err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: wsBuild.ID,
UpdatedAt: buildTime,
ProvisionerState: []byte{},
Deadline: c.deadline,
MaxDeadline: c.maxDeadline,
})
require.NoError(t, err)
wsBuild, err = db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
require.NoError(t, err)
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule)
require.NoError(t, err)
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
userQuietHoursStorePtr.Store(&userQuietHoursStore)
// Set the template policy.
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr)
templateScheduleStore.UseRestartRequirement.Store(true)
templateScheduleStore.TimeNowFn = func() time.Time {
return c.now
}
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
UserAutostartEnabled: false,
UserAutostopEnabled: false,
DefaultTTL: 0,
MaxTTL: 0,
UseRestartRequirement: true,
RestartRequirement: agplschedule.TemplateRestartRequirement{
// Every day
DaysOfWeek: 0b01111111,
Weeks: 0,
},
FailureTTL: 0,
InactivityTTL: 0,
LockedTTL: 0,
})
require.NoError(t, err)
// Check that the workspace build has the expected deadlines.
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
require.NoError(t, err)
if c.newDeadline.IsZero() {
c.newDeadline = wsBuild.Deadline
}
require.WithinDuration(t, c.newDeadline, newBuild.Deadline, time.Second)
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
})
}
}
func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
var (
org = dbgen.Organization(t, db, database.Organization{})
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{
CreatedBy: user.ID,
})
templateJob = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
OrganizationID: org.ID,
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{
"foo": "bar",
},
})
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: templateJob.ID,
})
template = dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
})
otherTemplate = dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
})
)
// Create a workspace that will be shared by two builds.
ws := dbgen.Workspace(t, db, database.Workspace{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
ctx := testutil.Context(t, testutil.WaitLong)
user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
ID: user.ID,
QuietHoursSchedule: userQuietHoursSchedule,
})
require.NoError(t, err)
realNow := time.Now().UTC()
nowY, nowM, nowD := realNow.Date()
buildTime := time.Date(nowY, nowM, nowD, 12, 0, 0, 0, time.UTC) // noon today UTC
now := time.Date(nowY, nowM, nowD, 18, 0, 0, 0, time.UTC) // 6pm today UTC
nextQuietHours := time.Date(nowY, nowM, nowD+1, 0, 0, 0, 0, time.UTC) // midnight tomorrow UTC
// A date very far in the future which would definitely be updated.
originalMaxDeadline := time.Date(nowY+1, nowM, nowD, 0, 0, 0, 0, time.UTC)
_ = otherTemplate
builds := []struct {
name string
templateID uuid.UUID
// Nil workspaceID means create a new workspace.
workspaceID uuid.UUID
buildNumber int32
buildStarted bool
buildCompleted bool
buildError bool
shouldBeUpdated bool
// Set below:
wsBuild database.WorkspaceBuild
}{
{
name: "DifferentTemplate",
templateID: otherTemplate.ID,
workspaceID: uuid.Nil,
buildNumber: 1,
buildStarted: true,
buildCompleted: true,
buildError: false,
shouldBeUpdated: false,
},
{
name: "NonStartedBuild",
templateID: template.ID,
workspaceID: uuid.Nil,
buildNumber: 1,
buildStarted: false,
buildCompleted: false,
buildError: false,
shouldBeUpdated: false,
},
{
name: "InProgressBuild",
templateID: template.ID,
workspaceID: uuid.Nil,
buildNumber: 1,
buildStarted: true,
buildCompleted: false,
buildError: false,
shouldBeUpdated: false,
},
{
name: "FailedBuild",
templateID: template.ID,
workspaceID: uuid.Nil,
buildNumber: 1,
buildStarted: true,
buildCompleted: true,
buildError: true,
shouldBeUpdated: false,
},
{
name: "NonLatestBuild",
templateID: template.ID,
workspaceID: ws.ID,
buildNumber: 1,
buildStarted: true,
buildCompleted: true,
buildError: false,
// This build was successful but is not the latest build for this
// workspace, see the next build.
shouldBeUpdated: false,
},
{
name: "LatestBuild",
templateID: template.ID,
workspaceID: ws.ID,
buildNumber: 2,
buildStarted: true,
buildCompleted: true,
buildError: false,
shouldBeUpdated: true,
},
{
name: "LatestBuildOtherWorkspace",
templateID: template.ID,
workspaceID: uuid.Nil,
buildNumber: 1,
buildStarted: true,
buildCompleted: true,
buildError: false,
shouldBeUpdated: true,
},
}
for i, b := range builds {
wsID := b.workspaceID
if wsID == uuid.Nil {
ws := dbgen.Workspace(t, db, database.Workspace{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: b.templateID,
})
wsID = ws.ID
}
job := dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
OrganizationID: org.ID,
FileID: file.ID,
InitiatorID: user.ID,
Provisioner: database.ProvisionerTypeEcho,
Tags: database.StringMap{
wsID.String(): "yeah",
},
})
wsBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: wsID,
BuildNumber: b.buildNumber,
JobID: job.ID,
InitiatorID: user.ID,
TemplateVersionID: templateVersion.ID,
})
err := db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: wsBuild.ID,
UpdatedAt: buildTime,
ProvisionerState: []byte{},
Deadline: originalMaxDeadline,
MaxDeadline: originalMaxDeadline,
})
require.NoError(t, err)
wsBuild, err = db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
require.NoError(t, err)
builds[i].wsBuild = wsBuild
if !b.buildStarted {
continue
}
acquiredJob, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
StartedAt: sql.NullTime{
Time: buildTime,
Valid: true,
},
WorkerID: uuid.NullUUID{
UUID: uuid.New(),
Valid: true,
},
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, wsID)),
})
require.NoError(t, err)
require.Equal(t, job.ID, acquiredJob.ID)
if !b.buildCompleted {
continue
}
buildError := ""
if b.buildError {
buildError = "error"
}
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: job.ID,
CompletedAt: sql.NullTime{
Time: buildTime,
Valid: true,
},
Error: sql.NullString{
String: buildError,
Valid: b.buildError,
},
UpdatedAt: buildTime,
})
require.NoError(t, err)
}
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule)
require.NoError(t, err)
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
userQuietHoursStorePtr.Store(&userQuietHoursStore)
// Set the template policy.
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr)
templateScheduleStore.UseRestartRequirement.Store(true)
templateScheduleStore.TimeNowFn = func() time.Time {
return now
}
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
UserAutostartEnabled: false,
UserAutostopEnabled: false,
DefaultTTL: 0,
MaxTTL: 0,
UseRestartRequirement: true,
RestartRequirement: agplschedule.TemplateRestartRequirement{
// Every day
DaysOfWeek: 0b01111111,
Weeks: 0,
},
FailureTTL: 0,
InactivityTTL: 0,
LockedTTL: 0,
})
require.NoError(t, err)
// Check each build.
for i, b := range builds {
msg := fmt.Sprintf("build %d: %s", i, b.name)
newBuild, err := db.GetWorkspaceBuildByID(ctx, b.wsBuild.ID)
require.NoError(t, err)
if b.shouldBeUpdated {
assert.WithinDuration(t, nextQuietHours, newBuild.Deadline, time.Second, msg)
assert.WithinDuration(t, nextQuietHours, newBuild.MaxDeadline, time.Second, msg)
} else {
assert.WithinDuration(t, originalMaxDeadline, newBuild.Deadline, time.Second, msg)
assert.WithinDuration(t, originalMaxDeadline, newBuild.MaxDeadline, time.Second, msg)
}
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/coder/coder/coderd/database"
agpl "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/tracing"
)
// enterpriseUserQuietHoursScheduleStore provides an
@@ -29,7 +30,8 @@ func NewEnterpriseUserQuietHoursScheduleStore(defaultSchedule string) (agpl.User
defaultSchedule: defaultSchedule,
}
_, err := s.parseSchedule(defaultSchedule)
// The context is only used for tracing so using a background ctx is fine.
_, err := s.parseSchedule(context.Background(), defaultSchedule)
if err != nil {
return nil, xerrors.Errorf("parse default schedule: %w", err)
}
@@ -37,7 +39,10 @@ func NewEnterpriseUserQuietHoursScheduleStore(defaultSchedule string) (agpl.User
return s, nil
}
func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(ctx context.Context, rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
_, span := tracing.StartSpan(ctx)
defer span.End()
userSet := true
if strings.TrimSpace(rawSchedule) == "" {
userSet = false
@@ -64,16 +69,22 @@ func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(rawSchedule string
}
func (s *enterpriseUserQuietHoursScheduleStore) Get(ctx context.Context, db database.Store, userID uuid.UUID) (agpl.UserQuietHoursScheduleOptions, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
user, err := db.GetUserByID(ctx, userID)
if err != nil {
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("get user by ID: %w", err)
}
return s.parseSchedule(user.QuietHoursSchedule)
return s.parseSchedule(ctx, user.QuietHoursSchedule)
}
func (s *enterpriseUserQuietHoursScheduleStore) Set(ctx context.Context, db database.Store, userID uuid.UUID, rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
opts, err := s.parseSchedule(rawSchedule)
ctx, span := tracing.StartSpan(ctx)
defer span.End()
opts, err := s.parseSchedule(ctx, rawSchedule)
if err != nil {
return opts, err
}
@@ -91,8 +102,12 @@ func (s *enterpriseUserQuietHoursScheduleStore) Set(ctx context.Context, db data
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("update user quiet hours schedule: %w", err)
}
// TODO(@dean): update max_deadline for all active builds for this user to clamp to
// the new schedule.
// We don't update workspace build deadlines when the user changes their own
// quiet hours schedule, because they could potentially keep their workspace
// running forever.
//
// Workspace build deadlines are updated when the template admin changes the
// template's settings however.
return opts, nil
}