Skip to content

Commit

Permalink
Use Go SDK Iterators when listing resources with the CLI (#1202)
Browse files Browse the repository at this point in the history
## Changes
Currently, when the CLI run a list API call (like list jobs), it uses
the `List*All` methods from the SDK, which list all resources in the
collection. This is very slow for large collections: if you need to list
all jobs from a workspace that has 10,000+ jobs, you'll be waiting for
at least 100 RPCs to complete before seeing any output.

Instead of using List*All() methods, the SDK recently added an iterator
data structure that allows traversing the collection without needing to
completely list it first. New pages are fetched lazily if the next
requested item belongs to the next page. Using the List() methods that
return these iterators, the CLI can proactively print out some of the
response before the complete collection has been fetched.

This involves a pretty major rewrite of the rendering logic in `cmdio`.
The idea there is to define custom rendering logic based on the type of
the provided resource. There are three renderer interfaces:

1. textRenderer: supports printing something in a textual format (i.e.
not JSON, and not templated).
2. jsonRenderer: supports printing something in a pretty-printed JSON
format.
3. templateRenderer: supports printing something using a text template.

There are also three renderer implementations:

1. readerRenderer: supports printing a reader. This only implements the
textRenderer interface.
2. iteratorRenderer: supports printing a `listing.Iterator` from the Go
SDK. This implements jsonRenderer and templateRenderer, buffering 20
resources at a time before writing them to the output.
3. defaultRenderer: supports printing arbitrary resources (the previous
implementation).

Callers will either use `cmdio.Render()` for rendering individual
resources or `io.Reader` or `cmdio.RenderIterator()` for rendering an
iterator. This separate method is needed to safely be able to match on
the type of the iterator, since Go does not allow runtime type matches
on generic types with an existential type parameter.

One other change that needs to happen is to split the templates used for
text representation of list resources into a header template and a row
template. The template is now executed multiple times for List API
calls, but the header should only be printed once. To support this, I
have added `headerTemplate` to `cmdIO`, and I have also changed
`RenderWithTemplate` to include a `headerTemplate` parameter everywhere.

## Tests
- [x] Unit tests for text rendering logic
- [x] Unit test for reflection-based iterator construction.

---------

Co-authored-by: Andrew Nester <[email protected]>
  • Loading branch information
mgyucht and andrewnester authored Feb 21, 2024
1 parent 5309e0f commit b65ce75
Show file tree
Hide file tree
Showing 89 changed files with 714 additions and 519 deletions.
12 changes: 9 additions & 3 deletions .codegen/service.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,22 @@ func init() {
// end service {{.Name}}{{end}}

{{- define "method-call" -}}
{{if .Response}}response, err :={{else}}err ={{end}} {{if .Service.IsAccounts}}a{{else}}w{{end}}.{{(.Service.TrimPrefix "account").PascalName}}.{{.PascalName}}{{if .Pagination}}All{{end}}(ctx{{if .Request}}, {{.CamelName}}Req{{end}})
{{if .Response -}}
response{{ if not .Pagination}}, err{{end}} :=
{{- else -}}
err =
{{- end}} {{if .Service.IsAccounts}}a{{else}}w{{end}}.{{(.Service.TrimPrefix "account").PascalName}}.{{.PascalName}}(ctx{{if .Request}}, {{.CamelName}}Req{{end}})
{{- if not (and .Response .Pagination) }}
if err != nil {
return err
}
{{- end}}
{{ if .Response -}}
{{- if .IsResponseByteStream -}}
defer response.{{.ResponseBodyField.PascalName}}.Close()
return cmdio.RenderReader(ctx, response.{{.ResponseBodyField.PascalName}})
return cmdio.Render{{ if .Pagination}}Iterator{{end}}(ctx, response.{{.ResponseBodyField.PascalName}})
{{- else -}}
return cmdio.Render(ctx, response)
return cmdio.Render{{ if .Pagination}}Iterator{{end}}(ctx, response)
{{- end -}}
{{ else -}}
return nil
Expand Down
92 changes: 46 additions & 46 deletions bundle/schema/docs/bundle_descriptions.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cmd/account/billable-usage/billable-usage.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/budgets/budgets.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/custom-app-integration/custom-app-integration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/groups/groups.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/ip-access-lists/ip-access-lists.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/log-delivery/log-delivery.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/metastore-assignments/metastore-assignments.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/metastores/metastores.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions cmd/account/network-connectivity/network-connectivity.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/o-auth-published-apps/o-auth-published-apps.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/service-principals/service-principals.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/users/users.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/account/workspace-assignment/workspace-assignment.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmd/fs/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func newCatCommand() *cobra.Command {
if err != nil {
return err
}
return cmdio.RenderReader(ctx, r)
return cmdio.Render(ctx, r)
}

return cmd
Expand Down
4 changes: 2 additions & 2 deletions cmd/fs/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (c *copy) emitFileSkippedEvent(sourcePath, targetPath string) error {
event := newFileSkippedEvent(fullSourcePath, fullTargetPath)
template := "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n"

return cmdio.RenderWithTemplate(c.ctx, event, template)
return cmdio.RenderWithTemplate(c.ctx, event, "", template)
}

func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error {
Expand All @@ -123,7 +123,7 @@ func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error {
event := newFileCopiedEvent(fullSourcePath, fullTargetPath)
template := "{{.SourcePath}} -> {{.TargetPath}}\n"

return cmdio.RenderWithTemplate(c.ctx, event, template)
return cmdio.RenderWithTemplate(c.ctx, event, "", template)
}

func newCpCommand() *cobra.Command {
Expand Down
4 changes: 2 additions & 2 deletions cmd/fs/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ func newLsCommand() *cobra.Command {

// Use template for long mode if the flag is set
if long {
return cmdio.RenderWithTemplate(ctx, jsonDirEntries, cmdio.Heredoc(`
return cmdio.RenderWithTemplate(ctx, jsonDirEntries, "", cmdio.Heredoc(`
{{range .}}{{if .IsDir}}DIRECTORY {{else}}FILE {{end}}{{.Size}} {{.ModTime|pretty_date}} {{.Name}}
{{end}}
`))
}
return cmdio.RenderWithTemplate(ctx, jsonDirEntries, cmdio.Heredoc(`
return cmdio.RenderWithTemplate(ctx, jsonDirEntries, "", cmdio.Heredoc(`
{{range .}}{{.Name}}
{{end}}
`))
Expand Down
2 changes: 1 addition & 1 deletion cmd/labs/project/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (cp *proxy) renderJsonAsTable(cmd *cobra.Command, args []string, envs map[s
}
// IntelliJ eagerly replaces tabs with spaces, even though we're not asking for it
fixedTemplate := strings.ReplaceAll(cp.TableTemplate, "\\t", "\t")
return cmdio.RenderWithTemplate(ctx, anyVal, fixedTemplate)
return cmdio.RenderWithTemplate(ctx, anyVal, "", fixedTemplate)
}

func (cp *proxy) commandInput(cmd *cobra.Command) ([]string, error) {
Expand Down
5 changes: 3 additions & 2 deletions cmd/root/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ func OutputType(cmd *cobra.Command) flags.Output {
}

func (f *outputFlag) initializeIO(cmd *cobra.Command) error {
var template string
var headerTemplate, template string
if cmd.Annotations != nil {
// rely on zeroval being an empty string
template = cmd.Annotations["template"]
headerTemplate = cmd.Annotations["headerTemplate"]
}

cmdIO := cmdio.NewIO(f.output, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), template)
cmdIO := cmdio.NewIO(f.output, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), headerTemplate, template)
ctx := cmdio.InContext(cmd.Context(), cmdIO)
cmd.SetContext(ctx)
return nil
Expand Down
7 changes: 2 additions & 5 deletions cmd/workspace/catalogs/catalogs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cmd/workspace/catalogs/overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
)

func listOverride(listCmd *cobra.Command) {
listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(`
{{header "Name"}} {{header "Type"}} {{header "Comment"}}`)
listCmd.Annotations["template"] = cmdio.Heredoc(`
{{header "Name"}} {{header "Type"}} {{header "Comment"}}
{{range .}}{{.Name|green}} {{blue "%s" .CatalogType}} {{.Comment}}
{{end}}`)
}
Expand Down
7 changes: 2 additions & 5 deletions cmd/workspace/clean-rooms/clean-rooms.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions cmd/workspace/cluster-policies/cluster-policies.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions cmd/workspace/clusters/clusters.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b65ce75

Please sign in to comment.