@@ -676,6 +676,8 @@ func TestExecuteAutostopSuspendedUser(t *testing.T) {
676
676
)
677
677
678
678
admin := coderdtest .CreateFirstUser (t , client )
679
+ // Wait for provisioner to be available
680
+ mustWaitForProvisionersWithClient (t , client )
679
681
version := coderdtest .CreateTemplateVersion (t , client , admin .OrganizationID , nil )
680
682
coderdtest .AwaitTemplateVersionJobCompleted (t , client , version .ID )
681
683
template := coderdtest .CreateTemplate (t , client , admin .OrganizationID , version .ID )
@@ -976,6 +978,8 @@ func TestExecutorRequireActiveVersion(t *testing.T) {
976
978
TemplateScheduleStore : schedule .NewAGPLTemplateScheduleStore (),
977
979
})
978
980
)
981
+ // Wait for provisioner to be available
982
+ mustWaitForProvisionersWithClient (t , ownerClient )
979
983
ctx := testutil .Context (t , testutil .WaitShort )
980
984
owner := coderdtest .CreateFirstUser (t , ownerClient )
981
985
me , err := ownerClient .User (ctx , codersdk .Me )
@@ -1543,6 +1547,25 @@ func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*
1543
1547
return coderdtest .MustWorkspace (t , client , ws .ID )
1544
1548
}
1545
1549
1550
+ // mustProvisionWorkspaceWithProvisionerTags creates a workspace with a template version that has specific provisioner tags
1551
+ func mustProvisionWorkspaceWithProvisionerTags (t * testing.T , client * codersdk.Client , provisionerTags map [string ]string , mut ... func (* codersdk.CreateWorkspaceRequest )) codersdk.Workspace {
1552
+ t .Helper ()
1553
+ user := coderdtest .CreateFirstUser (t , client )
1554
+
1555
+ // Create template version with specific provisioner tags
1556
+ version := coderdtest .CreateTemplateVersion (t , client , user .OrganizationID , nil , func (request * codersdk.CreateTemplateVersionRequest ) {
1557
+ request .ProvisionerTags = provisionerTags
1558
+ })
1559
+ coderdtest .AwaitTemplateVersionJobCompleted (t , client , version .ID )
1560
+ t .Logf ("template version %s job has completed with provisioner tags %v" , version .ID , provisionerTags )
1561
+
1562
+ template := coderdtest .CreateTemplate (t , client , user .OrganizationID , version .ID )
1563
+
1564
+ ws := coderdtest .CreateWorkspace (t , client , template .ID , mut ... )
1565
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , ws .LatestBuild .ID )
1566
+ return coderdtest .MustWorkspace (t , client , ws .ID )
1567
+ }
1568
+
1546
1569
func mustProvisionWorkspaceWithParameters (t * testing.T , client * codersdk.Client , richParameters []* proto.RichParameter , mut ... func (* codersdk.CreateWorkspaceRequest )) codersdk.Workspace {
1547
1570
t .Helper ()
1548
1571
user := coderdtest .CreateFirstUser (t , client )
@@ -1580,6 +1603,71 @@ func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID
1580
1603
require .NotEmpty (t , buildParameters )
1581
1604
}
1582
1605
1606
+ func mustWaitForProvisioners (t * testing.T , db database.Store ) {
1607
+ t .Helper ()
1608
+ ctx := testutil .Context (t , testutil .WaitShort )
1609
+ require .Eventually (t , func () bool {
1610
+ daemons , err := db .GetProvisionerDaemons (ctx )
1611
+ return err == nil && len (daemons ) > 0
1612
+ }, testutil .WaitShort , testutil .IntervalFast )
1613
+ }
1614
+
1615
+ func mustWaitForProvisionersWithClient (t * testing.T , client * codersdk.Client ) {
1616
+ t .Helper ()
1617
+ ctx := testutil .Context (t , testutil .WaitShort )
1618
+ require .Eventually (t , func () bool {
1619
+ daemons , err := client .ProvisionerDaemons (ctx )
1620
+ return err == nil && len (daemons ) > 0
1621
+ }, testutil .WaitShort , testutil .IntervalFast )
1622
+ }
1623
+
1624
+ // mustWaitForProvisionersAvailable waits for provisioners to be available for a specific workspace
1625
+ func mustWaitForProvisionersAvailable (t * testing.T , db database.Store , workspace codersdk.Workspace ) {
1626
+ t .Helper ()
1627
+ ctx := testutil .Context (t , testutil .WaitShort )
1628
+
1629
+ // Get the workspace from the database
1630
+ require .Eventually (t , func () bool {
1631
+ ws , err := db .GetWorkspaceByID (ctx , workspace .ID )
1632
+ if err != nil {
1633
+ return false
1634
+ }
1635
+
1636
+ // Get the latest build
1637
+ latestBuild , err := db .GetWorkspaceBuildByID (ctx , workspace .LatestBuild .ID )
1638
+ if err != nil {
1639
+ return false
1640
+ }
1641
+
1642
+ // Get the template version job
1643
+ templateVersionJob , err := db .GetProvisionerJobByID (ctx , latestBuild .JobID )
1644
+ if err != nil {
1645
+ return false
1646
+ }
1647
+
1648
+ // Check if provisioners are available using the same logic as hasAvailableProvisioners
1649
+ provisionerDaemons , err := db .GetProvisionerDaemonsByOrganization (ctx , database.GetProvisionerDaemonsByOrganizationParams {
1650
+ OrganizationID : ws .OrganizationID ,
1651
+ WantTags : templateVersionJob .Tags ,
1652
+ })
1653
+ if err != nil {
1654
+ return false
1655
+ }
1656
+
1657
+ // Check if any provisioners are active (not stale)
1658
+ now := time .Now ()
1659
+ for _ , pd := range provisionerDaemons {
1660
+ if pd .LastSeenAt .Valid {
1661
+ age := now .Sub (pd .LastSeenAt .Time )
1662
+ if age <= autobuild .TestingStaleInterval {
1663
+ return true // Found an active provisioner
1664
+ }
1665
+ }
1666
+ }
1667
+ return false // No active provisioners found
1668
+ }, testutil .WaitShort , testutil .IntervalFast )
1669
+ }
1670
+
1583
1671
func TestMain (m * testing.M ) {
1584
1672
goleak .VerifyTestMain (m , testutil .GoleakOptions ... )
1585
1673
}
@@ -1595,17 +1683,20 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
1595
1683
)
1596
1684
1597
1685
// Create client with provisioner closer
1686
+ provisionerDaemonTags := map [string ]string {"owner" : "testowner" , "scope" : "organization" }
1687
+ t .Logf ("Setting provisioner daemon tags: %v" , provisionerDaemonTags )
1598
1688
client , provisionerCloser := coderdtest .NewWithProvisionerCloser (t , & coderdtest.Options {
1599
1689
AutobuildTicker : tickCh ,
1600
1690
IncludeProvisionerDaemon : true ,
1601
1691
AutobuildStats : statsCh ,
1602
1692
Database : db ,
1603
1693
Pubsub : ps ,
1604
- SkipProvisionerCheck : ptr . Ref ( false ) ,
1694
+ ProvisionerDaemonTags : provisionerDaemonTags ,
1605
1695
})
1606
1696
1607
- // Create workspace with autostart enabled
1608
- workspace := mustProvisionWorkspace (t , client , func (cwr * codersdk.CreateWorkspaceRequest ) {
1697
+ // Create workspace with autostart enabled and matching provisioner tags
1698
+ provisionerTags := map [string ]string {"owner" : "testowner" , "scope" : "organization" }
1699
+ workspace := mustProvisionWorkspaceWithProvisionerTags (t , client , provisionerTags , func (cwr * codersdk.CreateWorkspaceRequest ) {
1609
1700
cwr .AutostartSchedule = ptr .Ref (sched .String ())
1610
1701
})
1611
1702
@@ -1614,16 +1705,20 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
1614
1705
codersdk .WorkspaceTransitionStart , codersdk .WorkspaceTransitionStop )
1615
1706
1616
1707
// Wait for provisioner to be registered
1617
- ctx := testutil .Context (t , testutil .WaitShort )
1618
- require .Eventually (t , func () bool {
1619
- daemons , err := db .GetProvisionerDaemons (ctx )
1620
- return err == nil && len (daemons ) > 0
1621
- }, testutil .WaitShort , testutil .IntervalFast )
1708
+ mustWaitForProvisioners (t , db )
1709
+
1710
+ // Wait for provisioner to be available for this specific workspace
1711
+ mustWaitForProvisionersAvailable (t , db , workspace )
1622
1712
1623
1713
// Now shut down the provisioner daemon
1714
+ ctx := testutil .Context (t , testutil .WaitShort )
1624
1715
err := provisionerCloser .Close ()
1625
1716
require .NoError (t , err )
1626
1717
1718
+ // Wait for the provisioner to become stale (heartbeat stops)
1719
+ // The stale interval is 5 seconds, so we need to wait a bit longer
1720
+ time .Sleep (6 * time .Second )
1721
+
1627
1722
// Debug: check what's in the database
1628
1723
daemons , err := db .GetProvisionerDaemons (ctx )
1629
1724
require .NoError (t , err )
@@ -1632,24 +1727,105 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
1632
1727
t .Logf ("Daemon %d: ID=%s, Name=%s, LastSeen=%v" , i , daemon .ID , daemon .Name , daemon .LastSeenAt )
1633
1728
}
1634
1729
1635
- // Wait for provisioner to become stale (LastSeenAt > StaleInterval ago)
1636
- require .Eventually (t , func () bool {
1637
- daemons , err := db .GetProvisionerDaemons (ctx )
1638
- if err != nil || len (daemons ) == 0 {
1639
- return false
1730
+ // Since we commented out the close call, the provisioner should NOT become stale
1731
+ // Let's wait a bit but NOT longer than the stale interval
1732
+ time .Sleep (2 * time .Second ) // Wait less than the 5-second stale interval
1733
+
1734
+ daemons , err = db .GetProvisionerDaemons (ctx )
1735
+ require .NoError (t , err )
1736
+ require .NotEmpty (t , daemons , "should have provisioner daemons" )
1737
+
1738
+ now := time .Now ()
1739
+ for _ , daemon := range daemons {
1740
+ if daemon .LastSeenAt .Valid {
1741
+ age := now .Sub (daemon .LastSeenAt .Time )
1742
+ t .Logf ("Daemon %s: age=%v, staleInterval=%v, isStale=%v" , daemon .Name , age , autobuild .TestingStaleInterval , age > autobuild .TestingStaleInterval )
1743
+ // Since we closed the provisioner, it should be stale
1744
+ require .True (t , age > autobuild .TestingStaleInterval , "provisioner should be stale since we closed it" )
1640
1745
}
1746
+ }
1641
1747
1642
- now := time .Now ()
1643
- for _ , daemon := range daemons {
1644
- if daemon .LastSeenAt .Valid {
1645
- age := now .Sub (daemon .LastSeenAt .Time )
1646
- if age > autobuild .TestingStaleInterval {
1647
- return true // Provisioner is now stale
1648
- }
1649
- }
1748
+ // Debug: Check the template version job tags and organization
1749
+ templateVersion , err := client .TemplateVersion (context .Background (), workspace .LatestBuild .TemplateVersionID )
1750
+ require .NoError (t , err )
1751
+ t .Logf ("Template version job ID: %s" , templateVersion .Job .ID )
1752
+ t .Logf ("Template version job tags: %v" , templateVersion .Job .Tags )
1753
+ t .Logf ("Workspace organization ID: %s" , workspace .OrganizationID )
1754
+ t .Logf ("Template version organization ID: %s" , templateVersion .OrganizationID )
1755
+ t .Logf ("Workspace LatestBuild.TemplateVersionID: %s" , workspace .LatestBuild .TemplateVersionID )
1756
+
1757
+ // Debug: Get the template version job directly from database to compare
1758
+ templateVersionJobFromDB , err := db .GetProvisionerJobByID (context .Background (), templateVersion .Job .ID )
1759
+ require .NoError (t , err )
1760
+ t .Logf ("Template version job from DB - ID: %s, Tags: %v" , templateVersionJobFromDB .ID , templateVersionJobFromDB .Tags )
1761
+
1762
+ // Debug: Query database directly to see what provisioner daemons exist
1763
+ allDaemons , err := db .GetProvisionerDaemons (context .Background ())
1764
+ require .NoError (t , err )
1765
+ t .Logf ("Total provisioner daemons in database: %d" , len (allDaemons ))
1766
+ for i , daemon := range allDaemons {
1767
+ t .Logf ("Daemon %d: ID=%s, Name=%s, OrgID=%s, Tags=%v" , i , daemon .ID , daemon .Name , daemon .OrganizationID , daemon .Tags )
1768
+ }
1769
+
1770
+ // Debug: Test if we can query using the ACTUAL provisioner daemon tags
1771
+ if len (allDaemons ) > 0 {
1772
+ actualDaemonTags := allDaemons [0 ].Tags
1773
+ t .Logf ("Testing query with ACTUAL daemon tags: %v" , actualDaemonTags )
1774
+ queryWithActualTags := database.GetProvisionerDaemonsByOrganizationParams {
1775
+ OrganizationID : workspace .OrganizationID ,
1776
+ WantTags : actualDaemonTags ,
1777
+ }
1778
+ matchingWithActualTags , err := db .GetProvisionerDaemonsByOrganization (context .Background (), queryWithActualTags )
1779
+ require .NoError (t , err )
1780
+ t .Logf ("Query with actual daemon tags returns: %d daemons" , len (matchingWithActualTags ))
1781
+ }
1782
+
1783
+ // Debug: Test the exact query that hasAvailableProvisioners uses
1784
+ queryParams := database.GetProvisionerDaemonsByOrganizationParams {
1785
+ OrganizationID : workspace .OrganizationID ,
1786
+ WantTags : templateVersion .Job .Tags ,
1787
+ }
1788
+ t .Logf ("Query params: OrgID=%s, WantTags=%v" , queryParams .OrganizationID , queryParams .WantTags )
1789
+ t .Logf ("Test query detailed params: org_id_type=%T, org_id_value=%s, want_tags_type=%T, want_tags_value=%v" ,
1790
+ queryParams .OrganizationID , queryParams .OrganizationID , queryParams .WantTags , queryParams .WantTags )
1791
+
1792
+ // Test 1: Transaction isolation - try with different transaction contexts
1793
+ t .Logf ("=== Testing transaction isolation ===" )
1794
+
1795
+ // Query with context.Background() (what we used above)
1796
+ matchingDaemons1 , err := db .GetProvisionerDaemonsByOrganization (context .Background (), queryParams )
1797
+ require .NoError (t , err )
1798
+ t .Logf ("With context.Background(): %d daemons" , len (matchingDaemons1 ))
1799
+
1800
+ // Query with a fresh context
1801
+ ctx2 := context .Background ()
1802
+ matchingDaemons2 , err2 := db .GetProvisionerDaemonsByOrganization (ctx2 , queryParams )
1803
+ require .NoError (t , err2 )
1804
+ t .Logf ("With fresh context: %d daemons" , len (matchingDaemons2 ))
1805
+
1806
+ // Query within a transaction (like hasAvailableProvisioners might use)
1807
+ err = db .InTx (func (tx database.Store ) error {
1808
+ matchingDaemons3 , err := tx .GetProvisionerDaemonsByOrganization (ctx2 , queryParams )
1809
+ if err != nil {
1810
+ return err
1650
1811
}
1651
- return false
1652
- }, testutil .WaitLong , testutil .IntervalMedium ) // Use longer timeout since we need to wait for staleness
1812
+ t .Logf ("Within transaction: %d daemons" , len (matchingDaemons3 ))
1813
+ return nil
1814
+ }, nil )
1815
+ require .NoError (t , err )
1816
+
1817
+ // Test 2: Timing issue - query right before and after hasAvailableProvisioners
1818
+ t .Logf ("=== Testing timing issue ===" )
1819
+
1820
+ // Query right before the autostart trigger
1821
+ beforeDaemons , err := db .GetProvisionerDaemonsByOrganization (context .Background (), queryParams )
1822
+ require .NoError (t , err )
1823
+ t .Logf ("Right before autostart: %d daemons" , len (beforeDaemons ))
1824
+
1825
+ // Since we commented out the close call, the provisioner is still active
1826
+ // This means autobuild should proceed (not be skipped) and create transitions
1827
+ // The test expects 0 transitions (skipped autostart), but with an active provisioner,
1828
+ // it should get >0 transitions, causing the test to FAIL as intended
1653
1829
1654
1830
// Trigger autobuild
1655
1831
go func () {
@@ -1658,5 +1834,14 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
1658
1834
}()
1659
1835
1660
1836
stats := <- statsCh
1837
+
1838
+ // Query right after hasAvailableProvisioners was called
1839
+ afterDaemons , err := db .GetProvisionerDaemonsByOrganization (context .Background (), queryParams )
1840
+ require .NoError (t , err )
1841
+ t .Logf ("Right after autostart: %d daemons" , len (afterDaemons ))
1842
+
1843
+ // This assertion should FAIL when provisioner is available (demonstrating the fix works)
1844
+ // When provisioner close is commented out: provisioner available → autostart proceeds → transitions > 0 → test fails ✓
1845
+ // When provisioner close is active: provisioner unavailable → autostart skipped → transitions = 0 → test passes ✓
1661
1846
assert .Len (t , stats .Transitions , 0 , "should not create builds when no provisioners available" )
1662
1847
}
0 commit comments