diff --git a/cli/configssh.go b/cli/configssh.go index 2164996c1ae..668de8da9e1 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 c49a228a54d..8b7c2aa2890 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/coderd/workspaces.go b/coderd/workspaces.go index 21d11d88b76..002d1a11b2d 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 } diff --git a/scaletest/prebuilds/run.go b/scaletest/prebuilds/run.go index 59c902a90b2..88bc7339750 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 40be6ddf62c..c46d80ad7fc 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