@@ -10,6 +10,8 @@ import (
1010"github.com/stretchr/testify/assert"
1111"github.com/stretchr/testify/require"
1212
13+ "cdr.dev/slog/sloggers/slogtest"
14+
1315"github.com/coder/coder/v2/coderd/database"
1416"github.com/coder/coder/v2/coderd/prebuilds"
1517"github.com/coder/coder/v2/testutil"
@@ -1527,6 +1529,152 @@ func TestCalculateDesiredInstances(t *testing.T) {
15271529}
15281530}
15291531
1532+ // TestCanSkipReconciliation ensures that CanSkipReconciliation only returns true
1533+ // when CalculateActions would return no actions.
1534+ func TestCanSkipReconciliation (t * testing.T ) {
1535+ t .Parallel ()
1536+
1537+ clock := quartz .NewMock (t )
1538+ logger := slogtest .Make (t ,nil )
1539+ backoffInterval := 5 * time .Minute
1540+
1541+ tests := []struct {
1542+ name string
1543+ preset database.GetTemplatePresetsWithPrebuildsRow
1544+ running []database.GetRunningPrebuiltWorkspacesRow
1545+ expired []database.GetRunningPrebuiltWorkspacesRow
1546+ inProgress []database.CountInProgressPrebuildsRow
1547+ pendingCount int
1548+ backoff * database.GetPresetsBackoffRow
1549+ expectedCanSkip bool
1550+ expectedActionNoOp bool
1551+ }{
1552+ {
1553+ name :"inactive_with_nothing_to_cleanup" ,
1554+ preset : database.GetTemplatePresetsWithPrebuildsRow {
1555+ UsingActiveVersion :false ,
1556+ Deleted :false ,
1557+ Deprecated :false ,
1558+ DesiredInstances : sql.NullInt32 {Int32 :5 ,Valid :true },
1559+ },
1560+ running : []database.GetRunningPrebuiltWorkspacesRow {},
1561+ expired : []database.GetRunningPrebuiltWorkspacesRow {},
1562+ inProgress : []database.CountInProgressPrebuildsRow {},
1563+ pendingCount :0 ,
1564+ backoff :nil ,
1565+ expectedCanSkip :true ,
1566+ expectedActionNoOp :true ,
1567+ },
1568+ {
1569+ name :"inactive_with_running_workspaces" ,
1570+ preset : database.GetTemplatePresetsWithPrebuildsRow {
1571+ UsingActiveVersion :false ,
1572+ Deleted :false ,
1573+ Deprecated :false ,
1574+ },
1575+ running : []database.GetRunningPrebuiltWorkspacesRow {
1576+ {ID :uuid .New ()},
1577+ },
1578+ expired : []database.GetRunningPrebuiltWorkspacesRow {},
1579+ inProgress : []database.CountInProgressPrebuildsRow {},
1580+ pendingCount :0 ,
1581+ backoff :nil ,
1582+ expectedCanSkip :false ,
1583+ expectedActionNoOp :false ,
1584+ },
1585+ {
1586+ name :"inactive_with_pending_jobs" ,
1587+ preset : database.GetTemplatePresetsWithPrebuildsRow {
1588+ UsingActiveVersion :false ,
1589+ Deleted :false ,
1590+ Deprecated :false ,
1591+ },
1592+ running : []database.GetRunningPrebuiltWorkspacesRow {},
1593+ expired : []database.GetRunningPrebuiltWorkspacesRow {},
1594+ inProgress : []database.CountInProgressPrebuildsRow {},
1595+ pendingCount :3 ,
1596+ backoff :nil ,
1597+ expectedCanSkip :false ,
1598+ expectedActionNoOp :false ,
1599+ },
1600+ {
1601+ name :"active_with_no_workspaces" ,
1602+ preset : database.GetTemplatePresetsWithPrebuildsRow {
1603+ UsingActiveVersion :true ,
1604+ Deleted :false ,
1605+ Deprecated :false ,
1606+ DesiredInstances : sql.NullInt32 {Int32 :5 ,Valid :true },
1607+ },
1608+ running : []database.GetRunningPrebuiltWorkspacesRow {},
1609+ expired : []database.GetRunningPrebuiltWorkspacesRow {},
1610+ inProgress : []database.CountInProgressPrebuildsRow {},
1611+ pendingCount :0 ,
1612+ backoff :nil ,
1613+ expectedCanSkip :false ,
1614+ expectedActionNoOp :false ,// Should create 5 workspaces
1615+ },
1616+ {
1617+ name :"active_with_backoff" ,
1618+ preset : database.GetTemplatePresetsWithPrebuildsRow {
1619+ UsingActiveVersion :true ,
1620+ Deleted :false ,
1621+ Deprecated :false ,
1622+ DesiredInstances : sql.NullInt32 {Int32 :5 ,Valid :true },
1623+ },
1624+ running : []database.GetRunningPrebuiltWorkspacesRow {},
1625+ expired : []database.GetRunningPrebuiltWorkspacesRow {},
1626+ inProgress : []database.CountInProgressPrebuildsRow {},
1627+ pendingCount :0 ,
1628+ backoff :& database.GetPresetsBackoffRow {
1629+ NumFailed :3 ,
1630+ LastBuildAt :clock .Now ().Add (- 1 * time .Minute ),
1631+ },
1632+ expectedCanSkip :false ,
1633+ expectedActionNoOp :false ,// Should backoff
1634+ },
1635+ }
1636+
1637+ for _ ,tt := range tests {
1638+ t .Run (tt .name ,func (t * testing.T ) {
1639+ t .Parallel ()
1640+
1641+ ps := prebuilds .NewPresetSnapshot (
1642+ tt .preset ,
1643+ []database.TemplateVersionPresetPrebuildSchedule {},
1644+ tt .running ,
1645+ tt .expired ,
1646+ tt .inProgress ,
1647+ tt .pendingCount ,
1648+ tt .backoff ,
1649+ false ,
1650+ clock ,
1651+ logger ,
1652+ )
1653+
1654+ canSkip := ps .CanSkipReconciliation ()
1655+ require .Equal (t ,tt .expectedCanSkip ,canSkip )
1656+
1657+ actions ,err := ps .CalculateActions (backoffInterval )
1658+ require .NoError (t ,err )
1659+
1660+ actionNoOp := true
1661+ for _ ,action := range actions {
1662+ if ! action .IsNoop () {
1663+ actionNoOp = false
1664+ break
1665+ }
1666+ }
1667+ require .Equal (t ,tt .expectedActionNoOp ,actionNoOp ,
1668+ "CalculateActions() isNoOp mismatch" )
1669+
1670+ // IMPORTANT: If CanSkipReconciliation is true, CalculateActions must return no actions
1671+ if canSkip {
1672+ require .True (t ,actionNoOp )
1673+ }
1674+ })
1675+ }
1676+ }
1677+
15301678func mustParseTime (t * testing.T ,layout ,value string ) time.Time {
15311679t .Helper ()
15321680parsedTime ,err := time .Parse (layout ,value )