From 554e32d71c5b885b683ea4e911244b3acaeebec0 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Tue, 11 Aug 2026 15:08:05 +0000 Subject: [PATCH 1/2] fix(cli,support,scaletest): terminate workspace paging on server count The workspaces endpoint applies its limit in SQL and then drops rows whose build or template the caller cannot read, so a page can be shorter than the requested limit without the result set being exhausted. Terminating on a short page truncated the results, and advancing the offset by the number of rows returned re-requested rows that had already been collected. Each loop now advances the offset by the requested page size and stops once it reaches the total the response reports. --- cli/configssh.go | 8 ++++++-- cli/exp_scaletest.go | 17 +++++++++++++---- scaletest/prebuilds/run.go | 6 +++++- support/support.go | 9 +++++++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cli/configssh.go b/cli/configssh.go index 2164996c1ae2e..668de8da9e17d 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -489,10 +489,14 @@ func (r *RootCmd) configSSH() *serpent.Command { for _, ws := range res.Workspaces { wsNames = append(wsNames, ws.Name) } - if len(res.Workspaces) < pageSize { + // The endpoint applies its limit in SQL and then drops rows whose + // build or template the caller cannot read, so a page shorter than + // pageSize does not mean the result set is exhausted. Count is the + // total before the limit and offset are applied. + offset += pageSize + if offset >= res.Count { break } - offset += pageSize } configOptions.workspaceNames = wsNames } diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go index c49a228a54d6d..8b7c2aa2890b8 100644 --- a/cli/exp_scaletest.go +++ b/cli/exp_scaletest.go @@ -577,7 +577,11 @@ func getScaletestPrebuildWorkspaces(ctx context.Context, client *codersdk.Client result = append(result, ws) } } - if len(resp.Workspaces) < pageSize { + // The endpoint applies its limit in SQL and then drops rows whose build + // or template the caller cannot read, so a page shorter than pageSize + // does not mean the result set is exhausted. Count is the total before + // the limit and offset are applied. + if (page+1)*pageSize >= resp.Count { break } } @@ -2249,9 +2253,6 @@ func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, owner, } pageNumber++ - if len(page.Workspaces) == 0 { - break - } pageWorkspaces := make([]codersdk.Workspace, 0, len(page.Workspaces)) for _, w := range page.Workspaces { @@ -2265,6 +2266,14 @@ func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, owner, pageWorkspaces = append(pageWorkspaces, w) } workspaces = append(workspaces, pageWorkspaces...) + + // The endpoint applies its limit in SQL and then drops rows whose build or + // template the caller cannot read, so a short or empty page does not mean + // the result set is exhausted. Count is the total before the limit and + // offset are applied. + if pageNumber*limit >= page.Count { + break + } } return workspaces, skipped, nil } diff --git a/scaletest/prebuilds/run.go b/scaletest/prebuilds/run.go index 59c902a90b2e1..88bc7339750d7 100644 --- a/scaletest/prebuilds/run.go +++ b/scaletest/prebuilds/run.go @@ -476,7 +476,11 @@ func allWorkspacesForTemplate(ctx context.Context, client *codersdk.Client, temp return nil, xerrors.Errorf("list workspaces page %d: %w", page, err) } workspaces = append(workspaces, resp.Workspaces...) - if len(resp.Workspaces) < pageSize { + // The endpoint applies its limit in SQL and then drops rows whose build or + // template the caller cannot read, so a page shorter than pageSize does not + // mean the result set is exhausted. Count is the total before the limit and + // offset are applied. + if (page+1)*pageSize >= resp.Count { break } } diff --git a/support/support.go b/support/support.go index 40be6ddf62c51..c46d80ad7fcd5 100644 --- a/support/support.go +++ b/support/support.go @@ -303,10 +303,15 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge } break } - if offset+len(resp.Workspaces) >= count || len(resp.Workspaces) == 0 { + // The offset advances by the requested limit rather than the number of + // rows returned. The endpoint applies its limit in SQL and then drops + // rows whose build or template the caller cannot read, so advancing by + // len(resp.Workspaces) would re-request rows already collected. Count is + // the total before the limit and offset are applied. + offset += limit + if offset >= count { break } - offset += len(resp.Workspaces) } if d.Workspaces != nil { // Replace with aggregated list From 47fd8bcbaa154652327844c174a50aa076f1af72 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Tue, 11 Aug 2026 19:06:49 +0000 Subject: [PATCH 2/2] chore(coderd): warn when workspaces are dropped from a list page --- coderd/workspaces.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 21d11d88b7636..002d1a11b2d00 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2820,6 +2820,10 @@ func convertWorkspaces( appStatusesByWorkspaceID[appStatus.WorkspaceID] = appStatus } + var ( + missingBuilds int + missingTemplates int + ) apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) for _, workspace := range workspaces { // If any data is missing from the workspace, just skip returning @@ -2829,10 +2833,12 @@ func convertWorkspaces( // fields? build, exists := buildByWorkspaceID[workspace.ID] if !exists { + missingBuilds++ continue } template, exists := templateByID[workspace.TemplateID] if !exists { + missingTemplates++ continue } appStatus := appStatusesByWorkspaceID[workspace.ID] @@ -2853,6 +2859,20 @@ func convertWorkspaces( apiWorkspaces = append(apiWorkspaces, w) } + + // Dropped workspaces make the returned slice shorter than the rows it was + // built from, which callers cannot distinguish from an exhausted result + // set when the rows came from a limited query. + if dropped := len(workspaces) - len(apiWorkspaces); dropped > 0 { + logger.Warn(ctx, "dropped workspaces missing build or template data", + slog.F("requester_id", requesterID), + slog.F("rows", len(workspaces)), + slog.F("dropped", dropped), + slog.F("missing_builds", missingBuilds), + slog.F("missing_templates", missingTemplates), + ) + } + return apiWorkspaces, nil }