mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
perf(coderd): build the workspace build fan-out maps once per batch (#28074)
`convertWorkspaceBuild` rebuilt seven maps from the caller's global slices on every call and rescanned the provisioner daemon rows to filter by job ID. `convertWorkspaceBuilds` calls it once per build with identical slices, so map construction cost `O(builds x rows)` where `O(rows)` suffices — quadratic in the number of workspaces on `GET /api/v2/workspaces`. The maps move into a `workspaceBuildIndex` built once per batch, keyed exactly as before and now including daemons by job ID. `convertWorkspaceBuild` takes the index instead of eight slices. Its parent already hoisted `workspaceByID`, `jobByID`, and `templateVersionByID` out of the same loop; this makes the rest consistent. Agents are sorted while the index is built, so a resource read by several builds is sorted once rather than once per build. Same comparator over the same rows, so the order is unchanged; `TestConvertWorkspaceBuildsAgentOrder` covers it. `BenchmarkConvertWorkspaceBuilds`, 5 resources x 2 agents x 4 apps per build: | builds | ns/op | B/op | allocs/op | | --- | --- | --- | --- | | 1 | 48.3k -> 48.1k | 121k -> 125k | 342 -> 359 | | 25 | 12.7M -> 1.34M | 38.0MB -> 3.2MB | 69,621 -> 8,347 | | 100 | 186M -> 5.67M | 596MB -> 13.0MB | 1,024,941 -> 33,080 | Single-build conversion is a wash (one extra struct allocation); the quadratic term is gone. Addresses the map-allocation half of PLAT-386 / #27205. Bounding the page size is separate (#28040) and does not remove this cost: at 100 workspaces per page it is still 100 passes over every resource, agent, app, script, log source, status, and daemon row in the page. --- Created with Coder Agents on behalf of @jscottmiller.
This commit is contained in:
+115
-89
@@ -1,6 +1,7 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
@@ -81,15 +82,17 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild,
|
||||
workspace,
|
||||
data.jobs[0],
|
||||
data.resources,
|
||||
data.metadata,
|
||||
data.agents,
|
||||
data.apps,
|
||||
data.appStatuses,
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
newWorkspaceBuildIndex(
|
||||
data.resources,
|
||||
data.metadata,
|
||||
data.agents,
|
||||
data.apps,
|
||||
data.appStatuses,
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
nil,
|
||||
),
|
||||
data.templateVersions[0],
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -291,15 +294,17 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
|
||||
workspaceBuild,
|
||||
workspace,
|
||||
data.jobs[0],
|
||||
data.resources,
|
||||
data.metadata,
|
||||
data.agents,
|
||||
data.apps,
|
||||
data.appStatuses,
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
newWorkspaceBuildIndex(
|
||||
data.resources,
|
||||
data.metadata,
|
||||
data.agents,
|
||||
data.apps,
|
||||
data.appStatuses,
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.provisionerDaemons,
|
||||
),
|
||||
data.templateVersions[0],
|
||||
data.provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -616,15 +621,8 @@ func (api *API) postWorkspaceBuildsInternal(
|
||||
*workspaceBuild,
|
||||
workspace,
|
||||
queuePos,
|
||||
[]database.WorkspaceResource{},
|
||||
[]database.WorkspaceResourceMetadatum{},
|
||||
[]database.WorkspaceAgent{},
|
||||
[]database.WorkspaceApp{},
|
||||
[]database.WorkspaceAppStatus{},
|
||||
[]database.GetWorkspaceAgentScriptsByAgentIDsRow{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
newWorkspaceBuildIndex(nil, nil, nil, nil, nil, nil, nil, provisionerDaemons),
|
||||
database.TemplateVersion{},
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceBuild{}, httperror.NewResponseError(
|
||||
@@ -1245,6 +1243,78 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
||||
}, nil
|
||||
}
|
||||
|
||||
// workspaceBuildIndex groups build rows by the ID they are looked up with.
|
||||
// Conversion reorders rows within a bucket in place, so an index must be read
|
||||
// by a single goroutine.
|
||||
type workspaceBuildIndex struct {
|
||||
resourcesByJobID map[uuid.UUID][]database.WorkspaceResource
|
||||
metadataByResourceID map[uuid.UUID][]database.WorkspaceResourceMetadatum
|
||||
agentsByResourceID map[uuid.UUID][]database.WorkspaceAgent
|
||||
appsByAgentID map[uuid.UUID][]database.WorkspaceApp
|
||||
statusesByAgentID map[uuid.UUID][]database.WorkspaceAppStatus
|
||||
scriptsByAgentID map[uuid.UUID][]database.GetWorkspaceAgentScriptsByAgentIDsRow
|
||||
logSourcesByAgentID map[uuid.UUID][]database.WorkspaceAgentLogSource
|
||||
daemonsByJobID map[uuid.UUID][]database.ProvisionerDaemon
|
||||
}
|
||||
|
||||
// newWorkspaceBuildIndex makes one pass over each slice, copying rows into
|
||||
// per-ID buckets. The input slices are neither modified nor aliased.
|
||||
func newWorkspaceBuildIndex(
|
||||
workspaceResources []database.WorkspaceResource,
|
||||
resourceMetadata []database.WorkspaceResourceMetadatum,
|
||||
resourceAgents []database.WorkspaceAgent,
|
||||
agentApps []database.WorkspaceApp,
|
||||
agentAppStatuses []database.WorkspaceAppStatus,
|
||||
agentScripts []database.GetWorkspaceAgentScriptsByAgentIDsRow,
|
||||
agentLogSources []database.WorkspaceAgentLogSource,
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||
) *workspaceBuildIndex {
|
||||
index := &workspaceBuildIndex{
|
||||
resourcesByJobID: make(map[uuid.UUID][]database.WorkspaceResource),
|
||||
metadataByResourceID: make(map[uuid.UUID][]database.WorkspaceResourceMetadatum),
|
||||
agentsByResourceID: make(map[uuid.UUID][]database.WorkspaceAgent),
|
||||
appsByAgentID: make(map[uuid.UUID][]database.WorkspaceApp),
|
||||
statusesByAgentID: make(map[uuid.UUID][]database.WorkspaceAppStatus),
|
||||
scriptsByAgentID: make(map[uuid.UUID][]database.GetWorkspaceAgentScriptsByAgentIDsRow),
|
||||
logSourcesByAgentID: make(map[uuid.UUID][]database.WorkspaceAgentLogSource),
|
||||
daemonsByJobID: make(map[uuid.UUID][]database.ProvisionerDaemon),
|
||||
}
|
||||
for _, resource := range workspaceResources {
|
||||
index.resourcesByJobID[resource.JobID] = append(index.resourcesByJobID[resource.JobID], resource)
|
||||
}
|
||||
for _, metadata := range resourceMetadata {
|
||||
index.metadataByResourceID[metadata.WorkspaceResourceID] = append(index.metadataByResourceID[metadata.WorkspaceResourceID], metadata)
|
||||
}
|
||||
for _, agent := range resourceAgents {
|
||||
index.agentsByResourceID[agent.ResourceID] = append(index.agentsByResourceID[agent.ResourceID], agent)
|
||||
}
|
||||
for _, app := range agentApps {
|
||||
index.appsByAgentID[app.AgentID] = append(index.appsByAgentID[app.AgentID], app)
|
||||
}
|
||||
for _, status := range agentAppStatuses {
|
||||
index.statusesByAgentID[status.AgentID] = append(index.statusesByAgentID[status.AgentID], status)
|
||||
}
|
||||
for _, script := range agentScripts {
|
||||
index.scriptsByAgentID[script.WorkspaceAgentID] = append(index.scriptsByAgentID[script.WorkspaceAgentID], script)
|
||||
}
|
||||
for _, logSource := range agentLogSources {
|
||||
index.logSourcesByAgentID[logSource.WorkspaceAgentID] = append(index.logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
|
||||
}
|
||||
for _, daemon := range provisionerDaemons {
|
||||
index.daemonsByJobID[daemon.JobID] = append(index.daemonsByJobID[daemon.JobID], daemon.ProvisionerDaemon)
|
||||
}
|
||||
// Agents within a resource are ordered by display order, then name.
|
||||
for _, agents := range index.agentsByResourceID {
|
||||
slices.SortFunc(agents, func(a, b database.WorkspaceAgent) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.DisplayOrder, b.DisplayOrder),
|
||||
cmp.Compare(a.Name, b.Name),
|
||||
)
|
||||
})
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func (api *API) convertWorkspaceBuilds(
|
||||
workspaceBuilds []database.WorkspaceBuild,
|
||||
workspaces []database.Workspace,
|
||||
@@ -1272,6 +1342,17 @@ func (api *API) convertWorkspaceBuilds(
|
||||
templateVersionByID[templateVersion.ID] = templateVersion
|
||||
}
|
||||
|
||||
index := newWorkspaceBuildIndex(
|
||||
workspaceResources,
|
||||
resourceMetadata,
|
||||
resourceAgents,
|
||||
agentApps,
|
||||
agentAppStatuses,
|
||||
agentScripts,
|
||||
agentLogSources,
|
||||
provisionerDaemons,
|
||||
)
|
||||
|
||||
// Should never be nil for API consistency
|
||||
apiBuilds := []codersdk.WorkspaceBuild{}
|
||||
for _, build := range workspaceBuilds {
|
||||
@@ -1292,15 +1373,8 @@ func (api *API) convertWorkspaceBuilds(
|
||||
build,
|
||||
workspace,
|
||||
job,
|
||||
workspaceResources,
|
||||
resourceMetadata,
|
||||
resourceAgents,
|
||||
agentApps,
|
||||
agentAppStatuses,
|
||||
agentScripts,
|
||||
agentLogSources,
|
||||
index,
|
||||
templateVersion,
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("converting workspace build: %w", err)
|
||||
@@ -1316,64 +1390,16 @@ func (api *API) convertWorkspaceBuild(
|
||||
build database.WorkspaceBuild,
|
||||
workspace database.Workspace,
|
||||
job database.GetProvisionerJobsByIDsWithQueuePositionRow,
|
||||
workspaceResources []database.WorkspaceResource,
|
||||
resourceMetadata []database.WorkspaceResourceMetadatum,
|
||||
resourceAgents []database.WorkspaceAgent,
|
||||
agentApps []database.WorkspaceApp,
|
||||
agentAppStatuses []database.WorkspaceAppStatus,
|
||||
agentScripts []database.GetWorkspaceAgentScriptsByAgentIDsRow,
|
||||
agentLogSources []database.WorkspaceAgentLogSource,
|
||||
index *workspaceBuildIndex,
|
||||
templateVersion database.TemplateVersion,
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||
) (codersdk.WorkspaceBuild, error) {
|
||||
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
|
||||
for _, resource := range workspaceResources {
|
||||
resourcesByJobID[resource.JobID] = append(resourcesByJobID[resource.JobID], resource)
|
||||
}
|
||||
metadataByResourceID := map[uuid.UUID][]database.WorkspaceResourceMetadatum{}
|
||||
for _, metadata := range resourceMetadata {
|
||||
metadataByResourceID[metadata.WorkspaceResourceID] = append(metadataByResourceID[metadata.WorkspaceResourceID], metadata)
|
||||
}
|
||||
agentsByResourceID := map[uuid.UUID][]database.WorkspaceAgent{}
|
||||
for _, agent := range resourceAgents {
|
||||
agentsByResourceID[agent.ResourceID] = append(agentsByResourceID[agent.ResourceID], agent)
|
||||
}
|
||||
appsByAgentID := map[uuid.UUID][]database.WorkspaceApp{}
|
||||
for _, app := range agentApps {
|
||||
appsByAgentID[app.AgentID] = append(appsByAgentID[app.AgentID], app)
|
||||
}
|
||||
scriptsByAgentID := map[uuid.UUID][]database.GetWorkspaceAgentScriptsByAgentIDsRow{}
|
||||
for _, script := range agentScripts {
|
||||
scriptsByAgentID[script.WorkspaceAgentID] = append(scriptsByAgentID[script.WorkspaceAgentID], script)
|
||||
}
|
||||
logSourcesByAgentID := map[uuid.UUID][]database.WorkspaceAgentLogSource{}
|
||||
for _, logSource := range agentLogSources {
|
||||
logSourcesByAgentID[logSource.WorkspaceAgentID] = append(logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
|
||||
}
|
||||
provisionerDaemonsForThisWorkspaceBuild := []database.ProvisionerDaemon{}
|
||||
for _, provisionerDaemon := range provisionerDaemons {
|
||||
if provisionerDaemon.JobID != job.ProvisionerJob.ID {
|
||||
continue
|
||||
}
|
||||
provisionerDaemonsForThisWorkspaceBuild = append(provisionerDaemonsForThisWorkspaceBuild, provisionerDaemon.ProvisionerDaemon)
|
||||
}
|
||||
matchedProvisioners := db2sdk.MatchedProvisioners(provisionerDaemonsForThisWorkspaceBuild, job.ProvisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
statusesByAgentID := map[uuid.UUID][]database.WorkspaceAppStatus{}
|
||||
for _, status := range agentAppStatuses {
|
||||
statusesByAgentID[status.AgentID] = append(statusesByAgentID[status.AgentID], status)
|
||||
}
|
||||
matchedProvisioners := db2sdk.MatchedProvisioners(index.daemonsByJobID[job.ProvisionerJob.ID], job.ProvisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
|
||||
resources := resourcesByJobID[job.ProvisionerJob.ID]
|
||||
resources := index.resourcesByJobID[job.ProvisionerJob.ID]
|
||||
apiResources := make([]codersdk.WorkspaceResource, 0)
|
||||
resourceAgentsMinOrder := map[uuid.UUID]int32{} // map[resource.ID]minOrder
|
||||
for _, resource := range resources {
|
||||
agents := agentsByResourceID[resource.ID]
|
||||
sort.Slice(agents, func(i, j int) bool {
|
||||
if agents[i].DisplayOrder != agents[j].DisplayOrder {
|
||||
return agents[i].DisplayOrder < agents[j].DisplayOrder
|
||||
}
|
||||
return agents[i].Name < agents[j].Name
|
||||
})
|
||||
agents := index.agentsByResourceID[resource.ID]
|
||||
|
||||
apiAgents := make([]codersdk.WorkspaceAgent, 0)
|
||||
resourceAgentsMinOrder[resource.ID] = math.MaxInt32
|
||||
@@ -1381,10 +1407,10 @@ func (api *API) convertWorkspaceBuild(
|
||||
for _, agent := range agents {
|
||||
resourceAgentsMinOrder[resource.ID] = min(resourceAgentsMinOrder[resource.ID], agent.DisplayOrder)
|
||||
|
||||
apps := appsByAgentID[agent.ID]
|
||||
scripts := scriptsByAgentID[agent.ID]
|
||||
statuses := statusesByAgentID[agent.ID]
|
||||
logSources := logSourcesByAgentID[agent.ID]
|
||||
apps := index.appsByAgentID[agent.ID]
|
||||
scripts := index.scriptsByAgentID[agent.ID]
|
||||
statuses := index.statusesByAgentID[agent.ID]
|
||||
logSources := index.logSourcesByAgentID[agent.ID]
|
||||
apiAgent, err := db2sdk.WorkspaceAgent(
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, statuses, agent, workspace.OwnerUsername, workspace.WorkspaceTable()), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout,
|
||||
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
|
||||
@@ -1394,7 +1420,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
}
|
||||
apiAgents = append(apiAgents, apiAgent)
|
||||
}
|
||||
metadata := append(make([]database.WorkspaceResourceMetadatum, 0), metadataByResourceID[resource.ID]...)
|
||||
metadata := append(make([]database.WorkspaceResourceMetadatum, 0), index.metadataByResourceID[resource.ID]...)
|
||||
apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata))
|
||||
}
|
||||
sort.Slice(apiResources, func(i, j int) bool {
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
)
|
||||
|
||||
// buildFixture holds one batch of rows shaped like the output of
|
||||
// workspaceBuildsData: a set of builds plus the flat resource, agent, app,
|
||||
// script, log source, status, and daemon rows for all of them.
|
||||
type buildFixture struct {
|
||||
builds []database.WorkspaceBuild
|
||||
workspaces []database.Workspace
|
||||
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
||||
templateVersions []database.TemplateVersion
|
||||
resources []database.WorkspaceResource
|
||||
metadata []database.WorkspaceResourceMetadatum
|
||||
agents []database.WorkspaceAgent
|
||||
apps []database.WorkspaceApp
|
||||
statuses []database.WorkspaceAppStatus
|
||||
scripts []database.GetWorkspaceAgentScriptsByAgentIDsRow
|
||||
logSources []database.WorkspaceAgentLogSource
|
||||
daemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
}
|
||||
|
||||
func newBuildFixture(builds, resourcesPerBuild int, agentsPerResource int32, appsPerAgent int) buildFixture {
|
||||
now := time.Date(2026, 8, 11, 0, 0, 0, 0, time.UTC)
|
||||
f := buildFixture{}
|
||||
for b := range builds {
|
||||
workspaceID := uuid.New()
|
||||
jobID := uuid.New()
|
||||
templateVersionID := uuid.New()
|
||||
|
||||
f.workspaces = append(f.workspaces, database.Workspace{
|
||||
ID: workspaceID,
|
||||
OwnerID: uuid.New(),
|
||||
OwnerUsername: fmt.Sprintf("owner-%d", b),
|
||||
Name: fmt.Sprintf("workspace-%d", b),
|
||||
})
|
||||
f.jobs = append(f.jobs, database.GetProvisionerJobsByIDsWithQueuePositionRow{
|
||||
ProvisionerJob: database.ProvisionerJob{
|
||||
ID: jobID,
|
||||
CreatedAt: now,
|
||||
StartedAt: sql.NullTime{Time: now, Valid: true},
|
||||
},
|
||||
})
|
||||
f.templateVersions = append(f.templateVersions, database.TemplateVersion{
|
||||
ID: templateVersionID,
|
||||
Name: fmt.Sprintf("version-%d", b),
|
||||
})
|
||||
f.builds = append(f.builds, database.WorkspaceBuild{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: workspaceID,
|
||||
JobID: jobID,
|
||||
TemplateVersionID: templateVersionID,
|
||||
BuildNumber: 1,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
f.daemons = append(f.daemons, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
|
||||
JobID: jobID,
|
||||
ProvisionerDaemon: database.ProvisionerDaemon{
|
||||
ID: uuid.New(),
|
||||
Name: fmt.Sprintf("daemon-%d", b),
|
||||
LastSeenAt: sql.NullTime{Time: now, Valid: true},
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
},
|
||||
})
|
||||
|
||||
for r := range resourcesPerBuild {
|
||||
resourceID := uuid.New()
|
||||
f.resources = append(f.resources, database.WorkspaceResource{
|
||||
ID: resourceID,
|
||||
JobID: jobID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Type: "example",
|
||||
Name: fmt.Sprintf("resource-%d-%d", b, r),
|
||||
})
|
||||
f.metadata = append(f.metadata, database.WorkspaceResourceMetadatum{
|
||||
WorkspaceResourceID: resourceID,
|
||||
Key: "key",
|
||||
Value: sql.NullString{String: "value", Valid: true},
|
||||
})
|
||||
|
||||
for a := range agentsPerResource {
|
||||
agentID := uuid.New()
|
||||
f.agents = append(f.agents, database.WorkspaceAgent{
|
||||
ID: agentID,
|
||||
ResourceID: resourceID,
|
||||
Name: fmt.Sprintf("agent-%d-%d-%d", b, r, a),
|
||||
DisplayOrder: agentsPerResource - a,
|
||||
APIKeyScope: database.AgentKeyScopeEnumAll,
|
||||
})
|
||||
f.scripts = append(f.scripts, database.GetWorkspaceAgentScriptsByAgentIDsRow{
|
||||
WorkspaceAgentID: agentID,
|
||||
Script: "echo hello",
|
||||
})
|
||||
f.logSources = append(f.logSources, database.WorkspaceAgentLogSource{
|
||||
WorkspaceAgentID: agentID,
|
||||
DisplayName: "startup",
|
||||
})
|
||||
|
||||
for p := range appsPerAgent {
|
||||
appID := uuid.New()
|
||||
f.apps = append(f.apps, database.WorkspaceApp{
|
||||
ID: appID,
|
||||
AgentID: agentID,
|
||||
Slug: fmt.Sprintf("app-%d-%d-%d-%d", b, r, a, p),
|
||||
DisplayName: "App",
|
||||
Health: database.WorkspaceAppHealthHealthy,
|
||||
SharingLevel: database.AppSharingLevelOwner,
|
||||
OpenIn: database.WorkspaceAppOpenInSlimWindow,
|
||||
})
|
||||
f.statuses = append(f.statuses, database.WorkspaceAppStatus{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
AppID: appID,
|
||||
State: database.WorkspaceAppStatusStateComplete,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func newConvertAPI() *API {
|
||||
api := &API{
|
||||
Options: &Options{
|
||||
AgentInactiveDisconnectTimeout: time.Minute,
|
||||
DeploymentValues: &codersdk.DeploymentValues{},
|
||||
},
|
||||
}
|
||||
coordinator := tailnet.NewCoordinator(slog.Logger{})
|
||||
api.TailnetCoordinator.Store(&coordinator)
|
||||
return api
|
||||
}
|
||||
|
||||
func (f buildFixture) convert(api *API) ([]codersdk.WorkspaceBuild, error) {
|
||||
return api.convertWorkspaceBuilds(
|
||||
f.builds,
|
||||
f.workspaces,
|
||||
f.jobs,
|
||||
f.resources,
|
||||
f.metadata,
|
||||
f.agents,
|
||||
f.apps,
|
||||
f.statuses,
|
||||
f.scripts,
|
||||
f.logSources,
|
||||
f.templateVersions,
|
||||
f.daemons,
|
||||
)
|
||||
}
|
||||
|
||||
// TestConvertWorkspaceBuildsAgentOrder covers the ordering that the shared
|
||||
// index is responsible for: agents come back sorted by display order. The
|
||||
// fixture assigns display order in reverse of agent name, so the expected
|
||||
// result is names in descending order.
|
||||
func TestConvertWorkspaceBuildsAgentOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
api := newConvertAPI()
|
||||
fixture := newBuildFixture(3, 2, 3, 1)
|
||||
|
||||
builds, err := fixture.convert(api)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, builds, 3)
|
||||
|
||||
for _, build := range builds {
|
||||
require.NotEmpty(t, build.Resources)
|
||||
for _, resource := range build.Resources {
|
||||
require.Len(t, resource.Agents, 3)
|
||||
for i := 1; i < len(resource.Agents); i++ {
|
||||
require.Greater(t, resource.Agents[i-1].Name, resource.Agents[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertWorkspaceBuildsAgentNameTiebreak covers the second half of the
|
||||
// agent comparison: agents sharing a display order are ordered by name.
|
||||
func TestConvertWorkspaceBuildsAgentNameTiebreak(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
api := newConvertAPI()
|
||||
fixture := newBuildFixture(1, 1, 3, 1)
|
||||
for i := range fixture.agents {
|
||||
fixture.agents[i].DisplayOrder = 0
|
||||
}
|
||||
|
||||
builds, err := fixture.convert(api)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, builds, 1)
|
||||
require.Len(t, builds[0].Resources, 1)
|
||||
|
||||
agents := builds[0].Resources[0].Agents
|
||||
require.Len(t, agents, 3)
|
||||
for i := 1; i < len(agents); i++ {
|
||||
require.Less(t, agents[i-1].Name, agents[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertWorkspaceBuildsRowsPerBuild asserts that a build receives only the
|
||||
// rows keyed to its own job and agents. The fixture encodes the build index in
|
||||
// app slugs, so apps read from another build are detectable by slug.
|
||||
func TestConvertWorkspaceBuildsRowsPerBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
buildCount = 3
|
||||
resourcesPerJob = 2
|
||||
agentsPerRes = 2
|
||||
appsPerAgent = 2
|
||||
)
|
||||
|
||||
api := newConvertAPI()
|
||||
fixture := newBuildFixture(buildCount, resourcesPerJob, agentsPerRes, appsPerAgent)
|
||||
|
||||
builds, err := fixture.convert(api)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, builds, buildCount)
|
||||
|
||||
for b, build := range builds {
|
||||
require.Len(t, build.Resources, resourcesPerJob)
|
||||
|
||||
for _, resource := range build.Resources {
|
||||
require.Equal(t, fixture.jobs[b].ProvisionerJob.ID, resource.JobID)
|
||||
require.Len(t, resource.Metadata, 1)
|
||||
require.Len(t, resource.Agents, agentsPerRes)
|
||||
|
||||
for _, agent := range resource.Agents {
|
||||
require.Len(t, agent.Scripts, 1)
|
||||
require.Len(t, agent.LogSources, 1)
|
||||
require.Len(t, agent.Apps, appsPerAgent)
|
||||
|
||||
for _, app := range agent.Apps {
|
||||
require.True(t, strings.HasPrefix(app.Slug, fmt.Sprintf("app-%d-", b)), app.Slug)
|
||||
require.Len(t, app.Statuses, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertWorkspaceBuildsMatchedProvisioners asserts that eligible daemons
|
||||
// are matched to the build whose job they were fetched for. The last build in
|
||||
// the batch has no daemon rows, so it reports zero counts while the others
|
||||
// report one.
|
||||
func TestConvertWorkspaceBuildsMatchedProvisioners(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
api := newConvertAPI()
|
||||
fixture := newBuildFixture(3, 1, 1, 1)
|
||||
fixture.daemons = fixture.daemons[:len(fixture.daemons)-1]
|
||||
|
||||
builds, err := fixture.convert(api)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, builds, 3)
|
||||
|
||||
for _, build := range builds[:2] {
|
||||
require.NotNil(t, build.MatchedProvisioners)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
||||
require.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
|
||||
last := builds[2]
|
||||
require.NotNil(t, last.MatchedProvisioners)
|
||||
require.Equal(t, 0, last.MatchedProvisioners.Count)
|
||||
require.Equal(t, 0, last.MatchedProvisioners.Available)
|
||||
require.False(t, last.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
|
||||
func BenchmarkConvertWorkspaceBuilds(b *testing.B) {
|
||||
for _, builds := range []int{1, 25, 100} {
|
||||
b.Run(fmt.Sprintf("Builds%d", builds), func(b *testing.B) {
|
||||
api := newConvertAPI()
|
||||
fixture := newBuildFixture(builds, 5, 2, 4)
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := fixture.convert(api); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -834,15 +834,8 @@ func createWorkspace(
|
||||
ProvisionerJob: *provisionerJob,
|
||||
QueuePosition: 0,
|
||||
},
|
||||
[]database.WorkspaceResource{},
|
||||
[]database.WorkspaceResourceMetadatum{},
|
||||
[]database.WorkspaceAgent{},
|
||||
[]database.WorkspaceApp{},
|
||||
[]database.WorkspaceAppStatus{},
|
||||
[]database.GetWorkspaceAgentScriptsByAgentIDsRow{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
newWorkspaceBuildIndex(nil, nil, nil, nil, nil, nil, nil, provisionerDaemons),
|
||||
database.TemplateVersion{},
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
|
||||
Reference in New Issue
Block a user