Skip to content

Commit 9af5e02

Browse files
test: add integration test
1 parent 63ffcc7 commit 9af5e02

File tree

4 files changed

+151
-11
lines changed

4 files changed

+151
-11
lines changed

coderd/prebuilds/global_snapshot.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package prebuilds
22

33
import (
4+
"github.com/coder/quartz"
45
"time"
56

67
"github.com/google/uuid"
@@ -18,6 +19,7 @@ type GlobalSnapshot struct {
1819
PrebuildsInProgress []database.CountInProgressPrebuildsRow
1920
Backoffs []database.GetPresetsBackoffRow
2021
HardLimitedPresetsMap map[uuid.UUID]database.GetPresetsAtFailureLimitRow
22+
clock quartz.Clock
2123
}
2224

2325
func NewGlobalSnapshot(
@@ -27,6 +29,7 @@ func NewGlobalSnapshot(
2729
prebuildsInProgress []database.CountInProgressPrebuildsRow,
2830
backoffs []database.GetPresetsBackoffRow,
2931
hardLimitedPresets []database.GetPresetsAtFailureLimitRow,
32+
clock quartz.Clock,
3033
) GlobalSnapshot {
3134
hardLimitedPresetsMap := make(map[uuid.UUID]database.GetPresetsAtFailureLimitRow, len(hardLimitedPresets))
3235
for _, preset := range hardLimitedPresets {
@@ -40,6 +43,7 @@ func NewGlobalSnapshot(
4043
PrebuildsInProgress: prebuildsInProgress,
4144
Backoffs: backoffs,
4245
HardLimitedPresetsMap: hardLimitedPresetsMap,
46+
clock: clock,
4347
}
4448
}
4549

@@ -88,6 +92,7 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err
8892
InProgress: inProgress,
8993
Backoff: backoffPtr,
9094
IsHardLimited: isHardLimited,
95+
clock: s.clock,
9196
}, nil
9297
}
9398

coderd/prebuilds/preset_snapshot.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type PresetSnapshot struct {
4646
InProgress []database.CountInProgressPrebuildsRow
4747
Backoff *database.GetPresetsBackoffRow
4848
IsHardLimited bool
49+
clock quartz.Clock
4950
}
5051

5152
// ReconciliationState represents the processed state of a preset's prebuilds,
@@ -155,9 +156,10 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState {
155156

156157
if p.isActive() {
157158
var err error
158-
desired, err = p.CalculateDesiredInstances(time.Now())
159+
desired, err = p.CalculateDesiredInstances(p.clock.Now())
159160
if err != nil {
160161
// TODO: handle error
162+
panic(err)
161163
}
162164
eligible = p.countEligible()
163165
extraneous = max(actual-expired-desired, 0)

coderd/prebuilds/preset_snapshot_test.go

Lines changed: 142 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestNoPrebuilds(t *testing.T) {
8484
preset(true, 0, current),
8585
}
8686

87-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil, nil)
87+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil, nil, quartz.NewMock(t))
8888
ps, err := snapshot.FilterByPreset(current.presetID)
8989
require.NoError(t, err)
9090

@@ -106,7 +106,7 @@ func TestNetNew(t *testing.T) {
106106
preset(true, 1, current),
107107
}
108108

109-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil, nil)
109+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil, nil, nil, quartz.NewMock(t))
110110
ps, err := snapshot.FilterByPreset(current.presetID)
111111
require.NoError(t, err)
112112

@@ -148,7 +148,7 @@ func TestOutdatedPrebuilds(t *testing.T) {
148148
var inProgress []database.CountInProgressPrebuildsRow
149149

150150
// WHEN: calculating the outdated preset's state.
151-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil)
151+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil, quartz.NewMock(t))
152152
ps, err := snapshot.FilterByPreset(outdated.presetID)
153153
require.NoError(t, err)
154154

@@ -214,7 +214,7 @@ func TestDeleteOutdatedPrebuilds(t *testing.T) {
214214
}
215215

216216
// WHEN: calculating the outdated preset's state.
217-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil)
217+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil, quartz.NewMock(t))
218218
ps, err := snapshot.FilterByPreset(outdated.presetID)
219219
require.NoError(t, err)
220220

@@ -459,7 +459,7 @@ func TestInProgressActions(t *testing.T) {
459459
}
460460

461461
// WHEN: calculating the current preset's state.
462-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil)
462+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil, quartz.NewMock(t))
463463
ps, err := snapshot.FilterByPreset(current.presetID)
464464
require.NoError(t, err)
465465

@@ -502,7 +502,7 @@ func TestExtraneous(t *testing.T) {
502502
var inProgress []database.CountInProgressPrebuildsRow
503503

504504
// WHEN: calculating the current preset's state.
505-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil)
505+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil, quartz.NewMock(t))
506506
ps, err := snapshot.FilterByPreset(current.presetID)
507507
require.NoError(t, err)
508508

@@ -683,7 +683,7 @@ func TestExpiredPrebuilds(t *testing.T) {
683683
}
684684

685685
// WHEN: calculating the current preset's state.
686-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, nil, nil, nil)
686+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, nil, nil, nil, quartz.NewMock(t))
687687
ps, err := snapshot.FilterByPreset(current.presetID)
688688
require.NoError(t, err)
689689

@@ -719,7 +719,7 @@ func TestDeprecated(t *testing.T) {
719719
var inProgress []database.CountInProgressPrebuildsRow
720720

721721
// WHEN: calculating the current preset's state.
722-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil)
722+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, nil, nil, quartz.NewMock(t))
723723
ps, err := snapshot.FilterByPreset(current.presetID)
724724
require.NoError(t, err)
725725

@@ -772,7 +772,7 @@ func TestLatestBuildFailed(t *testing.T) {
772772
}
773773

774774
// WHEN: calculating the current preset's state.
775-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, backoffs, nil)
775+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, running, inProgress, backoffs, nil, quartz.NewMock(t))
776776
psCurrent, err := snapshot.FilterByPreset(current.presetID)
777777
require.NoError(t, err)
778778

@@ -865,7 +865,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
865865
},
866866
}
867867

868-
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, inProgress, nil, nil)
868+
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, inProgress, nil, nil, quartz.NewMock(t))
869869

870870
// Nothing has to be created for preset 1.
871871
{
@@ -905,6 +905,129 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
905905
}
906906
}
907907

908+
func TestMultiplePresetsPerTemplateVersionV2(t *testing.T) {
909+
t.Parallel()
910+
911+
testCases := []struct {
912+
name string
913+
at time.Time
914+
expectedInstances []int32
915+
}{
916+
{
917+
name: "Before the 1st schedule",
918+
at: mustParseTime(t, time.RFC1123, "Mon, 02 Jun 2025 01:00:00 UTC"),
919+
expectedInstances: []int32{1, 1},
920+
},
921+
{
922+
name: "1st schedule",
923+
at: mustParseTime(t, time.RFC1123, "Mon, 02 Jun 2025 03:00:00 UTC"),
924+
expectedInstances: []int32{2, 1},
925+
},
926+
{
927+
name: "2nd schedule",
928+
at: mustParseTime(t, time.RFC1123, "Mon, 02 Jun 2025 07:00:00 UTC"),
929+
expectedInstances: []int32{3, 1},
930+
},
931+
{
932+
name: "3rd schedule",
933+
at: mustParseTime(t, time.RFC1123, "Mon, 02 Jun 2025 11:00:00 UTC"),
934+
expectedInstances: []int32{1, 4},
935+
},
936+
{
937+
name: "4th schedule",
938+
at: mustParseTime(t, time.RFC1123, "Mon, 02 Jun 2025 15:00:00 UTC"),
939+
expectedInstances: []int32{1, 5},
940+
},
941+
}
942+
943+
for _, tc := range testCases {
944+
t.Run(tc.name, func(t *testing.T) {
945+
t.Parallel()
946+
947+
templateID := uuid.New()
948+
templateVersionID := uuid.New()
949+
presetOpts1 := options{
950+
templateID: templateID,
951+
templateVersionID: templateVersionID,
952+
presetID: uuid.New(),
953+
presetName: "my-preset-1",
954+
prebuiltWorkspaceID: uuid.New(),
955+
workspaceName: "prebuilds1",
956+
}
957+
presetOpts2 := options{
958+
templateID: templateID,
959+
templateVersionID: templateVersionID,
960+
presetID: uuid.New(),
961+
presetName: "my-preset-2",
962+
prebuiltWorkspaceID: uuid.New(),
963+
workspaceName: "prebuilds2",
964+
}
965+
966+
clock := quartz.NewMock(t)
967+
clock.Set(tc.at)
968+
enableAutoscaling := func(preset database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
969+
preset.AutoscalingEnabled = true
970+
preset.AutoscalingTimezone = "UTC"
971+
return preset
972+
}
973+
presets := []database.GetTemplatePresetsWithPrebuildsRow{
974+
preset(true, 1, presetOpts1, enableAutoscaling),
975+
preset(true, 1, presetOpts2, enableAutoscaling),
976+
}
977+
schedules := []database.TemplateVersionPresetPrebuildSchedule{
978+
schedule(presets[0].ID, "* 2-4 * * 1-5", 2),
979+
schedule(presets[0].ID, "* 6-8 * * 1-5", 3),
980+
schedule(presets[1].ID, "* 10-12 * * 1-5", 4),
981+
schedule(presets[1].ID, "* 14-16 * * 1-5", 5),
982+
}
983+
984+
snapshot := prebuilds.NewGlobalSnapshot(presets, schedules, nil, nil, nil, nil, clock)
985+
986+
// Check 1st preset.
987+
{
988+
ps, err := snapshot.FilterByPreset(presetOpts1.presetID)
989+
require.NoError(t, err)
990+
991+
state := ps.CalculateState()
992+
actions, err := ps.CalculateActions(clock, backoffInterval)
993+
require.NoError(t, err)
994+
995+
validateState(t, prebuilds.ReconciliationState{
996+
Starting: 0,
997+
Desired: tc.expectedInstances[0],
998+
}, *state)
999+
validateActions(t, []*prebuilds.ReconciliationActions{
1000+
{
1001+
ActionType: prebuilds.ActionTypeCreate,
1002+
Create: tc.expectedInstances[0],
1003+
},
1004+
}, actions)
1005+
}
1006+
1007+
// Check 2nd preset.
1008+
{
1009+
ps, err := snapshot.FilterByPreset(presetOpts2.presetID)
1010+
require.NoError(t, err)
1011+
1012+
state := ps.CalculateState()
1013+
actions, err := ps.CalculateActions(clock, backoffInterval)
1014+
require.NoError(t, err)
1015+
1016+
validateState(t, prebuilds.ReconciliationState{
1017+
Starting: 0,
1018+
Desired: tc.expectedInstances[1],
1019+
}, *state)
1020+
validateActions(t, []*prebuilds.ReconciliationActions{
1021+
{
1022+
ActionType: prebuilds.ActionTypeCreate,
1023+
Create: tc.expectedInstances[1],
1024+
},
1025+
}, actions)
1026+
}
1027+
})
1028+
}
1029+
}
1030+
9081031
func TestMatchesCron(t *testing.T) {
9091032
t.Parallel()
9101033
testCases := []struct {
@@ -1294,6 +1417,15 @@ func preset(active bool, instances int32, opts options, muts ...func(row databas
12941417
return entry
12951418
}
12961419

1420+
func schedule(presetID uuid.UUID, cronExpr string, instances int32) database.TemplateVersionPresetPrebuildSchedule {
1421+
return database.TemplateVersionPresetPrebuildSchedule{
1422+
ID: uuid.New(),
1423+
PresetID: presetID,
1424+
CronExpression: cronExpr,
1425+
Instances: instances,
1426+
}
1427+
}
1428+
12971429
func prebuiltWorkspace(
12981430
opts options,
12991431
clock quartz.Clock,

enterprise/coderd/prebuilds/reconcile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ func (c *StoreReconciler) SnapshotState(ctx context.Context, store database.Stor
392392
allPrebuildsInProgress,
393393
presetsBackoff,
394394
hardLimitedPresets,
395+
c.clock,
395396
)
396397
return nil
397398
}, &database.TxOptions{

0 commit comments

Comments
 (0)