@@ -84,7 +84,7 @@ func TestNoPrebuilds(t *testing.T) {
8484preset (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 ) )
8888ps ,err := snapshot .FilterByPreset (current .presetID )
8989require .NoError (t ,err )
9090
@@ -106,7 +106,7 @@ func TestNetNew(t *testing.T) {
106106preset (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 ) )
110110ps ,err := snapshot .FilterByPreset (current .presetID )
111111require .NoError (t ,err )
112112
@@ -148,7 +148,7 @@ func TestOutdatedPrebuilds(t *testing.T) {
148148var 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 ) )
152152ps ,err := snapshot .FilterByPreset (outdated .presetID )
153153require .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 ) )
218218ps ,err := snapshot .FilterByPreset (outdated .presetID )
219219require .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 ) )
463463ps ,err := snapshot .FilterByPreset (current .presetID )
464464require .NoError (t ,err )
465465
@@ -502,7 +502,7 @@ func TestExtraneous(t *testing.T) {
502502var 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 ) )
506506ps ,err := snapshot .FilterByPreset (current .presetID )
507507require .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 ) )
687687ps ,err := snapshot .FilterByPreset (current .presetID )
688688require .NoError (t ,err )
689689
@@ -719,7 +719,7 @@ func TestDeprecated(t *testing.T) {
719719var 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 ) )
723723ps ,err := snapshot .FilterByPreset (current .presetID )
724724require .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 ) )
776776psCurrent ,err := snapshot .FilterByPreset (current .presetID )
777777require .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+
9081031func TestMatchesCron (t * testing.T ) {
9091032t .Parallel ()
9101033testCases := []struct {
@@ -1294,6 +1417,15 @@ func preset(active bool, instances int32, opts options, muts ...func(row databas
12941417return 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+
12971429func prebuiltWorkspace (
12981430opts options ,
12991431clock quartz.Clock ,