Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitcda9208

Browse files
authored
test: add ReconcileAll tests for multiple actions on expired prebuilds (#18265)
## DescriptionAdds tests for `ReconcileAll` to verify the full reconciliation flowwhen handling expired prebuilds. This complements existing lower-leveltests by checking multiple reconciliation actions (delete + create) atthe higher reconciliation cycle level.Related with comment:#17996 (comment)
1 parent5df70a6 commitcda9208

File tree

1 file changed

+295
-4
lines changed

1 file changed

+295
-4
lines changed

‎enterprise/coderd/prebuilds/reconcile_test.go

Lines changed: 295 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"fmt"
7+
"sort"
78
"sync"
89
"testing"
910
"time"
@@ -1429,6 +1430,244 @@ func TestTrackResourceReplacement(t *testing.T) {
14291430
require.EqualValues(t,1,metric.GetCounter().GetValue())
14301431
}
14311432

1433+
funcTestExpiredPrebuildsMultipleActions(t*testing.T) {
1434+
t.Parallel()
1435+
1436+
if!dbtestutil.WillUsePostgres() {
1437+
t.Skip("This test requires postgres")
1438+
}
1439+
1440+
testCases:= []struct {
1441+
namestring
1442+
runningint
1443+
desiredint32
1444+
expiredint
1445+
extraneousint
1446+
createdint
1447+
}{
1448+
// With 2 running prebuilds, none of which are expired, and the desired count is met,
1449+
// no deletions or creations should occur.
1450+
{
1451+
name:"no expired prebuilds - no actions taken",
1452+
running:2,
1453+
desired:2,
1454+
expired:0,
1455+
extraneous:0,
1456+
created:0,
1457+
},
1458+
// With 2 running prebuilds, 1 of which is expired, the expired prebuild should be deleted,
1459+
// and one new prebuild should be created to maintain the desired count.
1460+
{
1461+
name:"one expired prebuild – deleted and replaced",
1462+
running:2,
1463+
desired:2,
1464+
expired:1,
1465+
extraneous:0,
1466+
created:1,
1467+
},
1468+
// With 2 running prebuilds, both expired, both should be deleted,
1469+
// and 2 new prebuilds created to match the desired count.
1470+
{
1471+
name:"all prebuilds expired – all deleted and recreated",
1472+
running:2,
1473+
desired:2,
1474+
expired:2,
1475+
extraneous:0,
1476+
created:2,
1477+
},
1478+
// With 4 running prebuilds, 2 of which are expired, and the desired count is 2,
1479+
// the expired prebuilds should be deleted. No new creations are needed
1480+
// since removing the expired ones brings actual = desired.
1481+
{
1482+
name:"expired prebuilds deleted to reach desired count",
1483+
running:4,
1484+
desired:2,
1485+
expired:2,
1486+
extraneous:0,
1487+
created:0,
1488+
},
1489+
// With 4 running prebuilds (1 expired), and the desired count is 2,
1490+
// the first action should delete the expired one,
1491+
// and the second action should delete one additional (non-expired) prebuild
1492+
// to eliminate the remaining excess.
1493+
{
1494+
name:"expired prebuild deleted first, then extraneous",
1495+
running:4,
1496+
desired:2,
1497+
expired:1,
1498+
extraneous:1,
1499+
created:0,
1500+
},
1501+
}
1502+
1503+
for_,tc:=rangetestCases {
1504+
t.Run(tc.name,func(t*testing.T) {
1505+
t.Parallel()
1506+
1507+
clock:=quartz.NewMock(t)
1508+
ctx:=testutil.Context(t,testutil.WaitLong)
1509+
cfg:= codersdk.PrebuildsConfig{}
1510+
logger:=slogtest.Make(
1511+
t,&slogtest.Options{IgnoreErrors:true},
1512+
).Leveled(slog.LevelDebug)
1513+
db,pubSub:=dbtestutil.NewDB(t)
1514+
fakeEnqueuer:=newFakeEnqueuer()
1515+
registry:=prometheus.NewRegistry()
1516+
controller:=prebuilds.NewStoreReconciler(db,pubSub,cfg,logger,clock,registry,fakeEnqueuer)
1517+
1518+
// Set up test environment with a template, version, and preset
1519+
ownerID:=uuid.New()
1520+
dbgen.User(t,db, database.User{
1521+
ID:ownerID,
1522+
})
1523+
org,template:=setupTestDBTemplate(t,db,ownerID,false)
1524+
templateVersionID:=setupTestDBTemplateVersion(ctx,t,clock,db,pubSub,org.ID,ownerID,template.ID)
1525+
1526+
ttlDuration:=muchEarlier-time.Hour
1527+
ttl:=int32(-ttlDuration.Seconds())
1528+
preset:=setupTestDBPreset(t,db,templateVersionID,tc.desired,"b0rked",withTTL(ttl))
1529+
1530+
// The implementation uses time.Since(prebuild.CreatedAt) > ttl to check a prebuild expiration.
1531+
// Since our mock clock defaults to a fixed time, we must align it with the current time
1532+
// to ensure time-based logic works correctly in tests.
1533+
clock.Set(time.Now())
1534+
1535+
runningWorkspaces:=make(map[string]database.WorkspaceTable)
1536+
nonExpiredWorkspaces:=make([]database.WorkspaceTable,0,tc.running-tc.expired)
1537+
expiredWorkspaces:=make([]database.WorkspaceTable,0,tc.expired)
1538+
expiredCount:=0
1539+
forr:=rangetc.running {
1540+
// Space out createdAt timestamps by 1 second to ensure deterministic ordering.
1541+
// This lets the test verify that the correct (oldest) extraneous prebuilds are deleted.
1542+
createdAt:=muchEarlier+time.Duration(r)*time.Second
1543+
isExpired:=false
1544+
iftc.expired>expiredCount {
1545+
// Set createdAt far enough in the past so that time.Since(createdAt) > TTL,
1546+
// ensuring the prebuild is treated as expired in the test.
1547+
createdAt=ttlDuration-1*time.Minute
1548+
isExpired=true
1549+
expiredCount++
1550+
}
1551+
1552+
workspace,_:=setupTestDBPrebuild(
1553+
t,
1554+
clock,
1555+
db,
1556+
pubSub,
1557+
database.WorkspaceTransitionStart,
1558+
database.ProvisionerJobStatusSucceeded,
1559+
org.ID,
1560+
preset,
1561+
template.ID,
1562+
templateVersionID,
1563+
withCreatedAt(clock.Now().Add(createdAt)),
1564+
)
1565+
ifisExpired {
1566+
expiredWorkspaces=append(expiredWorkspaces,workspace)
1567+
}else {
1568+
nonExpiredWorkspaces=append(nonExpiredWorkspaces,workspace)
1569+
}
1570+
runningWorkspaces[workspace.ID.String()]=workspace
1571+
}
1572+
1573+
getJobStatusMap:=func(workspaces []database.WorkspaceTable)map[database.ProvisionerJobStatus]int {
1574+
jobStatusMap:=make(map[database.ProvisionerJobStatus]int)
1575+
for_,workspace:=rangeworkspaces {
1576+
workspaceBuilds,err:=db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
1577+
WorkspaceID:workspace.ID,
1578+
})
1579+
require.NoError(t,err)
1580+
1581+
for_,workspaceBuild:=rangeworkspaceBuilds {
1582+
job,err:=db.GetProvisionerJobByID(ctx,workspaceBuild.JobID)
1583+
require.NoError(t,err)
1584+
jobStatusMap[job.JobStatus]++
1585+
}
1586+
}
1587+
returnjobStatusMap
1588+
}
1589+
1590+
// Assert that the build associated with the given workspace has a 'start' transition status.
1591+
isWorkspaceStarted:=func(workspace database.WorkspaceTable) {
1592+
workspaceBuilds,err:=db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
1593+
WorkspaceID:workspace.ID,
1594+
})
1595+
require.NoError(t,err)
1596+
require.Equal(t,1,len(workspaceBuilds))
1597+
require.Equal(t,database.WorkspaceTransitionStart,workspaceBuilds[0].Transition)
1598+
}
1599+
1600+
// Assert that the workspace build history includes a 'start' followed by a 'delete' transition status.
1601+
isWorkspaceDeleted:=func(workspace database.WorkspaceTable) {
1602+
workspaceBuilds,err:=db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
1603+
WorkspaceID:workspace.ID,
1604+
})
1605+
require.NoError(t,err)
1606+
require.Equal(t,2,len(workspaceBuilds))
1607+
require.Equal(t,database.WorkspaceTransitionDelete,workspaceBuilds[0].Transition)
1608+
require.Equal(t,database.WorkspaceTransitionStart,workspaceBuilds[1].Transition)
1609+
}
1610+
1611+
// Verify that all running workspaces, whether expired or not, have successfully started.
1612+
workspaces,err:=db.GetWorkspacesByTemplateID(ctx,template.ID)
1613+
require.NoError(t,err)
1614+
require.Equal(t,tc.running,len(workspaces))
1615+
jobStatusMap:=getJobStatusMap(workspaces)
1616+
require.Len(t,workspaces,tc.running)
1617+
require.Len(t,jobStatusMap,1)
1618+
require.Equal(t,tc.running,jobStatusMap[database.ProvisionerJobStatusSucceeded])
1619+
1620+
// Assert that all running workspaces (expired and non-expired) have a 'start' transition state.
1621+
for_,workspace:=rangerunningWorkspaces {
1622+
isWorkspaceStarted(workspace)
1623+
}
1624+
1625+
// Trigger reconciliation to process expired prebuilds and enforce desired state.
1626+
require.NoError(t,controller.ReconcileAll(ctx))
1627+
1628+
// Sort non-expired workspaces by CreatedAt in ascending order (oldest first)
1629+
sort.Slice(nonExpiredWorkspaces,func(i,jint)bool {
1630+
returnnonExpiredWorkspaces[i].CreatedAt.Before(nonExpiredWorkspaces[j].CreatedAt)
1631+
})
1632+
1633+
// Verify the status of each non-expired workspace:
1634+
// - the oldest `tc.extraneous` should have been deleted (i.e., have a 'delete' transition),
1635+
// - while the remaining newer ones should still be running (i.e., have a 'start' transition).
1636+
extraneousCount:=0
1637+
for_,running:=rangenonExpiredWorkspaces {
1638+
ifextraneousCount<tc.extraneous {
1639+
isWorkspaceDeleted(running)
1640+
extraneousCount++
1641+
}else {
1642+
isWorkspaceStarted(running)
1643+
}
1644+
}
1645+
require.Equal(t,tc.extraneous,extraneousCount)
1646+
1647+
// Verify that each expired workspace has a 'delete' transition recorded,
1648+
// confirming it was properly marked for cleanup after reconciliation.
1649+
for_,expired:=rangeexpiredWorkspaces {
1650+
isWorkspaceDeleted(expired)
1651+
}
1652+
1653+
// After handling expired prebuilds, if running < desired, new prebuilds should be created.
1654+
// Verify that the correct number of new prebuild workspaces were created and started.
1655+
allWorkspaces,err:=db.GetWorkspacesByTemplateID(ctx,template.ID)
1656+
require.NoError(t,err)
1657+
1658+
createdCount:=0
1659+
for_,workspace:=rangeallWorkspaces {
1660+
if_,ok:=runningWorkspaces[workspace.ID.String()];!ok {
1661+
// Count and verify only the newly created workspaces (i.e., not part of the original running set)
1662+
isWorkspaceStarted(workspace)
1663+
createdCount++
1664+
}
1665+
}
1666+
require.Equal(t,tc.created,createdCount)
1667+
})
1668+
}
1669+
}
1670+
14321671
funcnewNoopEnqueuer()*notifications.NoopEnqueuer {
14331672
returnnotifications.NewNoopEnqueuer()
14341673
}
@@ -1538,22 +1777,42 @@ func setupTestDBTemplateVersion(
15381777
returntemplateVersion.ID
15391778
}
15401779

1780+
// Preset optional parameters.
1781+
// presetOptions defines a function type for modifying InsertPresetParams.
1782+
typepresetOptionsfunc(*database.InsertPresetParams)
1783+
1784+
// withTTL returns a presetOptions function that sets the invalidate_after_secs (TTL) field in InsertPresetParams.
1785+
funcwithTTL(ttlint32)presetOptions {
1786+
returnfunc(p*database.InsertPresetParams) {
1787+
p.InvalidateAfterSecs= sql.NullInt32{Valid:true,Int32:ttl}
1788+
}
1789+
}
1790+
15411791
funcsetupTestDBPreset(
15421792
t*testing.T,
15431793
db database.Store,
15441794
templateVersionID uuid.UUID,
15451795
desiredInstancesint32,
15461796
presetNamestring,
1797+
opts...presetOptions,
15471798
) database.TemplateVersionPreset {
15481799
t.Helper()
1549-
preset:=dbgen.Preset(t,db, database.InsertPresetParams{
1800+
insertPresetParams:= database.InsertPresetParams{
15501801
TemplateVersionID:templateVersionID,
15511802
Name:presetName,
15521803
DesiredInstances: sql.NullInt32{
15531804
Valid:true,
15541805
Int32:desiredInstances,
15551806
},
1556-
})
1807+
}
1808+
1809+
// Apply optional parameters to insertPresetParams (e.g., TTL).
1810+
for_,opt:=rangeopts {
1811+
opt(&insertPresetParams)
1812+
}
1813+
1814+
preset:=dbgen.Preset(t,db,insertPresetParams)
1815+
15571816
dbgen.PresetParameter(t,db, database.InsertPresetParametersParams{
15581817
TemplateVersionPresetID:preset.ID,
15591818
Names: []string{"test"},
@@ -1562,6 +1821,21 @@ func setupTestDBPreset(
15621821
returnpreset
15631822
}
15641823

1824+
// prebuildOptions holds optional parameters for creating a prebuild workspace.
1825+
typeprebuildOptionsstruct {
1826+
createdAt*time.Time
1827+
}
1828+
1829+
// prebuildOption defines a function type to apply optional settings to prebuildOptions.
1830+
typeprebuildOptionfunc(*prebuildOptions)
1831+
1832+
// withCreatedAt returns a prebuildOption that sets the CreatedAt timestamp.
1833+
funcwithCreatedAt(createdAt time.Time)prebuildOption {
1834+
returnfunc(opts*prebuildOptions) {
1835+
opts.createdAt=&createdAt
1836+
}
1837+
}
1838+
15651839
funcsetupTestDBPrebuild(
15661840
t*testing.T,
15671841
clock quartz.Clock,
@@ -1573,9 +1847,10 @@ func setupTestDBPrebuild(
15731847
preset database.TemplateVersionPreset,
15741848
templateID uuid.UUID,
15751849
templateVersionID uuid.UUID,
1850+
opts...prebuildOption,
15761851
) (database.WorkspaceTable, database.WorkspaceBuild) {
15771852
t.Helper()
1578-
returnsetupTestDBWorkspace(t,clock,db,ps,transition,prebuildStatus,orgID,preset,templateID,templateVersionID,agplprebuilds.SystemUserID,agplprebuilds.SystemUserID)
1853+
returnsetupTestDBWorkspace(t,clock,db,ps,transition,prebuildStatus,orgID,preset,templateID,templateVersionID,agplprebuilds.SystemUserID,agplprebuilds.SystemUserID,opts...)
15791854
}
15801855

15811856
funcsetupTestDBWorkspace(
@@ -1591,6 +1866,7 @@ func setupTestDBWorkspace(
15911866
templateVersionID uuid.UUID,
15921867
initiatorID uuid.UUID,
15931868
ownerID uuid.UUID,
1869+
opts...prebuildOption,
15941870
) (database.WorkspaceTable, database.WorkspaceBuild) {
15951871
t.Helper()
15961872
cancelledAt:= sql.NullTime{}
@@ -1618,15 +1894,30 @@ func setupTestDBWorkspace(
16181894
default:
16191895
}
16201896

1897+
// Apply all provided prebuild options.
1898+
prebuiltOptions:=&prebuildOptions{}
1899+
for_,opt:=rangeopts {
1900+
opt(prebuiltOptions)
1901+
}
1902+
1903+
// Set createdAt to default value if not overridden by options.
1904+
createdAt:=clock.Now().Add(muchEarlier)
1905+
ifprebuiltOptions.createdAt!=nil {
1906+
createdAt=*prebuiltOptions.createdAt
1907+
// Ensure startedAt matches createdAt for consistency.
1908+
startedAt= sql.NullTime{Time:createdAt,Valid:true}
1909+
}
1910+
16211911
workspace:=dbgen.Workspace(t,db, database.WorkspaceTable{
16221912
TemplateID:templateID,
16231913
OrganizationID:orgID,
16241914
OwnerID:ownerID,
16251915
Deleted:false,
1916+
CreatedAt:createdAt,
16261917
})
16271918
job:=dbgen.ProvisionerJob(t,db,ps, database.ProvisionerJob{
16281919
InitiatorID:initiatorID,
1629-
CreatedAt:clock.Now().Add(muchEarlier),
1920+
CreatedAt:createdAt,
16301921
StartedAt:startedAt,
16311922
CompletedAt:completedAt,
16321923
CanceledAt:cancelledAt,

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp