Files
coder/coderd/wsrelateddata_internal_test.go
T
Spike Curtis 6a58d50f1d feat(coderd): add queue-position-free provisioner job query (#28367)
Follow-up to #28302 ([GRU-82](https://linear.app/codercom/issue/GRU-82), [RFC](https://app.notion.com/p/coderhq/Lite-Workspace-API-Requests-3aad579be5928060b5e7cc65c539717f)). That PR added related-data selection but noted that selecting `latest_build.job` still ran the expensive `GetProvisionerJobsByIDsWithQueuePosition` query, which computes queue position and size with window functions over pending jobs and provisioner daemons and is consistently the top resource consumer in scale tests.

This PR adds a cheaper `GetProvisionerJobsByIDs` query that fetches jobs by ID without the queue-position computation, and selects between the two based on whether `latest_build.job.queue_position` is requested. When the queue position is not selected, `QueuePosition` and `QueueSize` are left zero and the rest of the response is unchanged.

Both paths go through a single `provisionerJobsByIDs` API method that takes the `jobRelated` selection, switches on it, and reshapes the cheaper query's rows into `GetProvisionerJobsByIDsWithQueuePositionRow` so downstream conversion is uniform.

The new query has the same dbauthz properties as the queue-position variant (system-level pass-through pending the proper provisioner-job RBAC check tracked in #16160), with a matching authorization test.

No behavior change for existing callers: they select `queue_position` (via `allLatestBuildRelated`), so they continue to use the queue-position query.


---

*Generated by Coder Agents on behalf of @spikecurtis.*
2026-08-24 16:17:55 +02:00

306 lines
11 KiB
Go

package coderd
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/testutil"
)
// TestAllWorkspaceRelated verifies that allWorkspaceRelated selects every node
// in the related-data hierarchy.
func TestAllWorkspaceRelated(t *testing.T) {
t.Parallel()
all := allWorkspaceRelated()
require.True(t, all.Template)
require.NotNil(t, all.LatestBuild)
require.NotNil(t, all.LatestBuild.Job)
require.True(t, all.LatestBuild.Job.QueuePosition)
require.True(t, all.LatestBuild.TemplateVersion)
require.NotNil(t, all.LatestBuild.Resources)
require.True(t, all.LatestBuild.Resources.Metadata)
require.NotNil(t, all.LatestBuild.Resources.Agents)
require.NotNil(t, all.LatestBuild.Resources.Agents.Apps)
require.True(t, all.LatestBuild.Resources.Agents.Apps.Statuses)
require.True(t, all.LatestBuild.Resources.Agents.Scripts)
require.True(t, all.LatestBuild.Resources.Agents.LogSources)
require.True(t, all.LatestBuild.appStatuses())
}
// TestLatestBuildRelatedAppStatuses verifies the nil-safe appStatuses accessor
// only reports true when the full apps.statuses path is present.
func TestLatestBuildRelatedAppStatuses(t *testing.T) {
t.Parallel()
require.False(t, (*latestBuildRelated)(nil).appStatuses())
require.False(t, (&latestBuildRelated{}).appStatuses())
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{}}).appStatuses())
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{}}}).appStatuses())
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{}}}}).appStatuses())
require.True(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}}).appStatuses())
}
// TestWorkspaceBuildsDataQueryGating asserts that workspaceBuildsData only
// issues the database queries implied by the selection. gomock is strict, so
// any query that is not set up here fails the test if invoked.
func TestWorkspaceBuildsDataQueryGating(t *testing.T) {
t.Parallel()
build := database.WorkspaceBuild{
ID: uuid.New(),
JobID: uuid.New(),
WorkspaceID: uuid.New(),
TemplateVersionID: uuid.New(),
}
resource := database.WorkspaceResource{ID: uuid.New(), JobID: build.JobID}
agent := database.WorkspaceAgent{ID: uuid.New(), ResourceID: resource.ID}
app := database.WorkspaceApp{ID: uuid.New(), AgentID: agent.ID}
expectJob := func(db *dbmock.MockStore) {
db.EXPECT().GetProvisionerJobsByIDs(gomock.Any(), gomock.Any()).
Return([]database.ProvisionerJob{}, nil)
db.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).
Return([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}, nil)
}
expectJobWithQueuePosition := func(db *dbmock.MockStore) {
db.EXPECT().GetProvisionerJobsByIDsWithQueuePosition(gomock.Any(), gomock.Any()).
Return([]database.GetProvisionerJobsByIDsWithQueuePositionRow{}, nil)
db.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).
Return([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}, nil)
}
expectTemplateVersion := func(db *dbmock.MockStore) {
db.EXPECT().GetTemplateVersionsByIDs(gomock.Any(), gomock.Any()).
Return([]database.TemplateVersion{}, nil)
}
expectResources := func(db *dbmock.MockStore) {
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceResource{resource}, nil)
}
expectAgents := func(db *dbmock.MockStore) {
db.EXPECT().GetWorkspaceAgentsByResourceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAgent{agent}, nil)
}
expectApps := func(db *dbmock.MockStore) {
db.EXPECT().GetWorkspaceAppsByAgentIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceApp{app}, nil)
}
cases := []struct {
name string
cfg latestBuildRelated
setup func(*dbmock.MockStore)
}{
{
name: "BuildOnly",
cfg: latestBuildRelated{},
setup: func(*dbmock.MockStore) {},
},
{
name: "Job",
cfg: latestBuildRelated{Job: &jobRelated{}},
setup: expectJob,
},
{
name: "JobWithQueuePosition",
cfg: latestBuildRelated{Job: &jobRelated{QueuePosition: true}},
setup: expectJobWithQueuePosition,
},
{
name: "TemplateVersion",
cfg: latestBuildRelated{TemplateVersion: true},
setup: expectTemplateVersion,
},
{
name: "Resources",
cfg: latestBuildRelated{Resources: &resourcesRelated{}},
setup: expectResources,
},
{
name: "Metadata",
cfg: latestBuildRelated{Resources: &resourcesRelated{Metadata: true}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
db.EXPECT().GetWorkspaceResourceMetadataByResourceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceResourceMetadatum{}, nil)
},
},
{
name: "Agents",
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{}}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
expectAgents(db)
},
},
{
name: "Apps",
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{}}}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
expectAgents(db)
expectApps(db)
},
},
{
name: "AppStatuses",
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
expectAgents(db)
expectApps(db)
db.EXPECT().GetWorkspaceAppStatusesByAppIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAppStatus{}, nil)
},
},
{
name: "Scripts",
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Scripts: true}}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
expectAgents(db)
db.EXPECT().GetWorkspaceAgentScriptsByAgentIDs(gomock.Any(), gomock.Any()).
Return([]database.GetWorkspaceAgentScriptsByAgentIDsRow{}, nil)
},
},
{
name: "LogSources",
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{LogSources: true}}},
setup: func(db *dbmock.MockStore) {
expectResources(db)
expectAgents(db)
db.EXPECT().GetWorkspaceAgentLogSourcesByAgentIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAgentLogSource{}, nil)
},
},
{
name: "All",
cfg: allLatestBuildRelated(),
setup: func(db *dbmock.MockStore) {
expectJobWithQueuePosition(db)
expectTemplateVersion(db)
expectResources(db)
expectAgents(db)
expectApps(db)
db.EXPECT().GetWorkspaceResourceMetadataByResourceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceResourceMetadatum{}, nil)
db.EXPECT().GetWorkspaceAppStatusesByAppIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAppStatus{}, nil)
db.EXPECT().GetWorkspaceAgentScriptsByAgentIDs(gomock.Any(), gomock.Any()).
Return([]database.GetWorkspaceAgentScriptsByAgentIDsRow{}, nil)
db.EXPECT().GetWorkspaceAgentLogSourcesByAgentIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAgentLogSource{}, nil)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
tc.setup(db)
api := &API{Options: &Options{Database: db}}
_, err := api.workspaceBuildsData(ctx, []database.WorkspaceBuild{build}, tc.cfg)
require.NoError(t, err)
})
}
}
// TestWorkspaceBuildsDataResourcesShortCircuit verifies that when resources are
// requested but none exist, no agent-level queries are issued even though the
// deeper nodes are selected.
func TestWorkspaceBuildsDataResourcesShortCircuit(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
build := database.WorkspaceBuild{ID: uuid.New(), JobID: uuid.New(), TemplateVersionID: uuid.New()}
// No resources returned: the agent, app, and status queries must be skipped.
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceResource{}, nil)
cfg := latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}}
api := &API{Options: &Options{Database: db}}
_, err := api.workspaceBuildsData(ctx, []database.WorkspaceBuild{build}, cfg)
require.NoError(t, err)
}
// TestWorkspaceDataQueryGating asserts that workspaceData gates its
// workspace-level queries (templates, latest builds, and latest app statuses)
// on the selection.
func TestWorkspaceDataQueryGating(t *testing.T) {
t.Parallel()
workspace := database.Workspace{ID: uuid.New(), TemplateID: uuid.New()}
cases := []struct {
name string
cfg workspaceRelated
setup func(*dbmock.MockStore)
}{
{
name: "None",
cfg: workspaceRelated{},
setup: func(*dbmock.MockStore) {},
},
{
name: "Template",
cfg: workspaceRelated{Template: true},
setup: func(db *dbmock.MockStore) {
db.EXPECT().GetTemplatesWithFilter(gomock.Any(), gomock.Any()).
Return([]database.Template{}, nil)
},
},
{
name: "LatestBuild",
cfg: workspaceRelated{LatestBuild: &latestBuildRelated{}},
setup: func(db *dbmock.MockStore) {
// No builds returned, so no build-subtree queries run.
db.EXPECT().GetLatestWorkspaceBuildsByWorkspaceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceBuild{}, nil)
},
},
{
name: "AppStatuses",
cfg: workspaceRelated{LatestBuild: &latestBuildRelated{
Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}},
}},
setup: func(db *dbmock.MockStore) {
// The workspace-level latest app status query is gated on the
// apps.statuses node. With no builds, only the workspace-level
// status, build, and (empty) resource queries run.
db.EXPECT().GetLatestWorkspaceAppStatusesByWorkspaceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceAppStatus{}, nil)
db.EXPECT().GetLatestWorkspaceBuildsByWorkspaceIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceBuild{}, nil)
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
Return([]database.WorkspaceResource{}, nil)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
tc.setup(db)
api := &API{Options: &Options{Database: db}}
_, err := api.workspaceData(ctx, []database.Workspace{workspace}, tc.cfg)
require.NoError(t, err)
})
}
}