Skip to content

Commit a48fb99

Browse files
fix: nullify next_start_at on schedule update
1 parent d349c56 commit a48fb99

File tree

5 files changed

+147
-1
lines changed

5 files changed

+147
-1
lines changed

coderd/autobuild/lifecycle_executor.go

+21
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,27 @@ func (e *Executor) runOnce(t time.Time) Stats {
205205
return xerrors.Errorf("get template scheduling options: %w", err)
206206
}
207207

208+
// If next start at is not valid we need to re-compute it.
209+
if !ws.NextStartAt.Valid && ws.AutostartSchedule.Valid {
210+
next, err := schedule.NextAllowedAutostart(currentTick, ws.AutostartSchedule.String, templateSchedule)
211+
if err != nil {
212+
return xerrors.Errorf("compute next allowed autostart: %w", err)
213+
}
214+
215+
e.log.Debug(e.ctx, "computed next allowed", slog.F("currentTick", currentTick), slog.F("next", next), slog.F("UTC", next.UTC()))
216+
217+
nextStartAt := sql.NullTime{Valid: true, Time: next.UTC()}
218+
if err = tx.UpdateWorkspaceNextStartAt(e.ctx, database.UpdateWorkspaceNextStartAtParams{
219+
ID: wsID,
220+
NextStartAt: nextStartAt,
221+
}); err != nil {
222+
return xerrors.Errorf("update workspace next start at: %w", err)
223+
}
224+
225+
// Save re-fetching the workspace
226+
ws.NextStartAt = nextStartAt
227+
}
228+
208229
tmpl, err = tx.GetTemplateByID(e.ctx, ws.TemplateID)
209230
if err != nil {
210231
return xerrors.Errorf("get template by ID: %w", err)

coderd/database/dump.sql

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/migrations/000277_workspace_next_start_at.down.sql

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
DROP VIEW workspaces_expanded;
22

3+
DROP TRIGGER IF EXISTS trigger_update_workspaces_schedule ON workspaces;
4+
DROP FUNCTION IF EXISTS nullify_workspace_next_start_at;
5+
36
DROP INDEX workspace_next_start_at_idx;
47

58
ALTER TABLE ONLY workspaces DROP COLUMN IF EXISTS next_start_at;

coderd/database/migrations/000277_workspace_next_start_at.up.sql

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@ ALTER TABLE ONLY workspaces ADD COLUMN IF NOT EXISTS next_start_at TIMESTAMPTZ D
22

33
CREATE INDEX workspace_next_start_at_idx ON workspaces USING btree (next_start_at) WHERE (deleted=false);
44

5+
CREATE FUNCTION nullify_workspace_next_start_at() RETURNS trigger
6+
LANGUAGE plpgsql
7+
AS $$
8+
DECLARE
9+
BEGIN
10+
IF (NEW.autostart_schedule <> OLD.autostart_schedule) AND (NEW.next_start_at = OLD.next_start_at) THEN
11+
UPDATE workspaces
12+
SET next_start_at = NULL
13+
WHERE id = NEW.id;
14+
END IF;
15+
RETURN NEW;
16+
END;
17+
$$;
18+
19+
CREATE TRIGGER trigger_update_workspaces_schedule
20+
AFTER UPDATE ON workspaces
21+
FOR EACH ROW
22+
EXECUTE PROCEDURE nullify_workspace_next_start_at();
23+
524
-- Recreate view
625
DROP VIEW workspaces_expanded;
726

enterprise/coderd/workspaces_test.go

+88-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd_test
22

33
import (
44
"context"
5+
"database/sql"
56
"net/http"
67
"sync/atomic"
78
"testing"
@@ -18,8 +19,10 @@ import (
1819
"github.com/coder/coder/v2/coderd/autobuild"
1920
"github.com/coder/coder/v2/coderd/coderdtest"
2021
"github.com/coder/coder/v2/coderd/database"
22+
"github.com/coder/coder/v2/coderd/database/dbauthz"
2123
"github.com/coder/coder/v2/coderd/database/dbfake"
2224
"github.com/coder/coder/v2/coderd/database/dbtestutil"
25+
"github.com/coder/coder/v2/coderd/database/dbtime"
2326
"github.com/coder/coder/v2/coderd/notifications"
2427
"github.com/coder/coder/v2/coderd/rbac"
2528
agplschedule "github.com/coder/coder/v2/coderd/schedule"
@@ -1138,7 +1141,6 @@ func TestWorkspaceAutobuild(t *testing.T) {
11381141

11391142
// First create a template that only supports Monday-Friday
11401143
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID, func(ctr *codersdk.CreateTemplateRequest) {
1141-
ctr.AllowUserAutostart = ptr.Ref(true)
11421144
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.BitmapToWeekdays(0b00011111)}
11431145
})
11441146
require.Equal(t, version1.ID, template.ActiveVersionID)
@@ -1680,6 +1682,91 @@ func TestAdminViewAllWorkspaces(t *testing.T) {
16801682
require.Equal(t, 0, len(memberViewWorkspaces.Workspaces), "member in other org should see 0 workspaces")
16811683
}
16821684

1685+
func TestNextStartAtIsNullifiedOnScheduleChange(t *testing.T) {
1686+
t.Parallel()
1687+
1688+
var (
1689+
tickCh = make(chan time.Time)
1690+
statsCh = make(chan autobuild.Stats)
1691+
clock = quartz.NewMock(t)
1692+
)
1693+
1694+
// Set the clock to 8AM Monday, 1st January, 2024 to keep
1695+
// this test deterministic.
1696+
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
1697+
1698+
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
1699+
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
1700+
Options: &coderdtest.Options{
1701+
AutobuildTicker: tickCh,
1702+
IncludeProvisionerDaemon: true,
1703+
AutobuildStats: statsCh,
1704+
Logger: &logger,
1705+
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, clock),
1706+
},
1707+
LicenseOptions: &coderdenttest.LicenseOptions{
1708+
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
1709+
},
1710+
})
1711+
1712+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
1713+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1714+
1715+
// Create a template that allows autostart Monday-Sunday
1716+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
1717+
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.AllDaysOfWeek}
1718+
})
1719+
require.Equal(t, version.ID, template.ActiveVersionID)
1720+
1721+
// Create a workspace with a schedule Sunday-Saturday
1722+
sched, err := cron.Weekly("CRON_TZ=UTC 0 9 * * 0-6")
1723+
require.NoError(t, err)
1724+
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
1725+
cwr.AutostartSchedule = ptr.Ref(sched.String())
1726+
})
1727+
1728+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
1729+
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
1730+
1731+
// Check we have a 'NextStartAt'
1732+
require.NotNil(t, ws.NextStartAt)
1733+
1734+
// Create a new slightly different cron schedule that could
1735+
// potentially make NextStartAt invalid.
1736+
sched, err = cron.Weekly("CRON_TZ=UTC 0 9 * * 1-6")
1737+
require.NoError(t, err)
1738+
ctx := testutil.Context(t, testutil.WaitShort)
1739+
1740+
// We want to test the database nullifies the NextStartAt so we
1741+
// make a raw DB call here. We pass in NextStartAt here so we
1742+
// can test the database will nullify it and not us.
1743+
//nolint: gocritic // We need system context to modify this.
1744+
err = db.UpdateWorkspaceAutostart(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAutostartParams{
1745+
ID: ws.ID,
1746+
AutostartSchedule: sql.NullString{Valid: true, String: sched.String()},
1747+
NextStartAt: sql.NullTime{Valid: true, Time: *ws.NextStartAt},
1748+
})
1749+
require.NoError(t, err)
1750+
1751+
ws = coderdtest.MustWorkspace(t, client, ws.ID)
1752+
1753+
// Check 'NextStartAt' has been nullified
1754+
require.Nil(t, ws.NextStartAt)
1755+
1756+
// Now we let the lifecycle executor run. This should spot that the
1757+
// NextStartAt is null and update it for us.
1758+
next := dbtime.Now()
1759+
tickCh <- next
1760+
stats := <-statsCh
1761+
assert.Len(t, stats.Errors, 0)
1762+
assert.Len(t, stats.Transitions, 0)
1763+
1764+
// Ensure NextStartAt has been set, and is the expected value
1765+
ws = coderdtest.MustWorkspace(t, client, ws.ID)
1766+
require.NotNil(t, ws.NextStartAt)
1767+
require.Equal(t, sched.Next(next), ws.NextStartAt.UTC())
1768+
}
1769+
16831770
func must[T any](value T, err error) T {
16841771
if err != nil {
16851772
panic(err)

0 commit comments

Comments
 (0)