Skip to content

fix: use paginated resource listing to avoid gRPC message size limit#341

Merged
mangelajo merged 5 commits intojumpstarter-dev:mainfrom
raballew:020-grpc-message-size-limit
Mar 20, 2026
Merged

fix: use paginated resource listing to avoid gRPC message size limit#341
mangelajo merged 5 commits intojumpstarter-dev:mainfrom
raballew:020-grpc-message-size-limit

Conversation

@raballew
Copy link
Copy Markdown
Member

Summary

  • Implement client-side pagination for list_leases and list_exporters on ClientConfigV1Alpha1, iterating through all pages (page_size=100) instead of returning a single page
  • Remove duplicate single-page list_leases/list_exporters methods, keeping only the paginated versions
  • Add _collect_all_leases shared helper to avoid duplicating the pagination loop (also used by list_exporters when enriching exporters with lease data)

Fixes #337

Test plan

  • Unit tests: pagination across multiple pages for both leases and exporters (test_list_leases_paginates, test_list_exporters_paginates, test_list_leases_single_page)
  • Unit tests: CLI commands call paginated methods (test_get_exporters_calls_list_exporters, test_get_leases_calls_list_leases)
  • Unit tests: gRPC default options (test_default_options_preserve_existing_defaults, test_user_options_override_defaults)
  • E2E BATS test: create 101 leases and verify all are returned (paginated lease listing returns all leases)
  • E2E BATS test: create 101 exporters and verify all are returned (paginated exporter listing returns all exporters)

🤖 Generated with Claude Code

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 19, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit ddbce35
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69bc494c38a3fd0008100ecb
😎 Deploy Preview https://deploy-preview-341--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1efc7eda-4def-4d6a-abb4-0f43eaa33dbf

📥 Commits

Reviewing files that changed from the base of the PR and between 388517c and ddbce35.

📒 Files selected for processing (1)
  • e2e/tests.bats
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/tests.bats

📝 Walkthrough

Walkthrough

ClientConfig listing methods now aggregate paginated server responses for leases and exporters internally; tests added across e2e, CLI, config pagination unit tests, and gRPC options unit tests.

Changes

Cohort / File(s) Summary
E2E Pagination Tests
e2e/tests.bats
Added two Bats e2e tests that create 101 leases/exporters, run jmp get ... -o yaml, assert 101 name: entries, then clean up.
CLI Command Tests
python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py
Added unit tests invoking get_exporters and get_leases callbacks asserting the paginated config methods are called with expected arguments.
gRPC Options Tests
python/packages/jumpstarter/jumpstarter/common/grpc_test.py
Added tests for _override_default_grpc_options verifying defaults and that user-supplied options override defaults.
Client Pagination Implementation
python/packages/jumpstarter/jumpstarter/config/client.py
Added ClientConfigV1Alpha1._collect_all_leases(...); changed list_exporters(...) and list_leases(...) to use pagination loops (default page_size=100), aggregate pages, and return lists with next_page_token=None.
Client Pagination Unit Tests
python/packages/jumpstarter/jumpstarter/config/client_config_test.py
Added async tests mocking multi-page ListLeases/ListExporters responses, asserting page_size, page_token progression, and full aggregation across pages.

Sequence Diagram

sequenceDiagram
    participant CLI as Client (CLI)
    participant ClientConfig as ClientConfigV1Alpha1
    participant Paginator as _collect_all_leases()
    participant Service as gRPC Service

    CLI->>ClientConfig: list_leases(filter, only_active)
    ClientConfig->>Paginator: _collect_all_leases(svc, page_size=100, ...)
    loop while next_page_token != ""
        Paginator->>Service: ListLeases(page_token, page_size)
        Service-->>Paginator: LeaseList {leases, next_page_token}
        Paginator->>Paginator: append leases
    end
    Paginator-->>ClientConfig: LeaseList {all_leases, next_page_token=None}
    ClientConfig-->>CLI: LeaseList (aggregated)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #671: Modifies ClientConfigV1Alpha1 (pagination and listing behavior) — strong overlap with the pagination and list aggregation changes in this PR.
  • #558: Previously changed list_exporters/list_leases behavior and CLI handling — related to exporter/lease listing logic.

Suggested labels

backport release-0.7

Suggested reviewers

  • mangelajo
  • bennyz
  • bkhizgiy

Poem

🐇
I hop through pages, token in paw,
Gathering leases till lists are raw.
One by one the entries blend,
No more truncation at the end.
Full results — a joyful hop! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: implementing paginated resource listing to resolve gRPC message size limit issues.
Description check ✅ Passed The description provides clear implementation details aligned with the changeset, including the pagination approach, helper method addition, and comprehensive test coverage.
Linked Issues check ✅ Passed The PR directly addresses issue #337 by implementing pagination in list_leases and list_exporters to avoid gRPC message size limit errors [#337].
Out of Scope Changes check ✅ Passed All changes are scoped to pagination implementation: client-side pagination logic, helper method addition, unit tests, and E2E tests—all directly supporting the pagination solution for issue #337.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@raballew raballew force-pushed the 020-grpc-message-size-limit branch from 1e2cf60 to ca699bd Compare March 19, 2026 12:24
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
python/packages/jumpstarter/jumpstarter/common/grpc_test.py (1)

11-14: Consider also asserting that other defaults remain intact.

The test verifies the override works, but doesn't confirm that non-overridden defaults (e.g., grpc.lb_policy_name) are still present. Adding one more assertion would strengthen confidence that the merge behavior is correct.

💡 Suggested enhancement
 def test_user_options_override_defaults():
     user_options = {"grpc.keepalive_time_ms": 50000}
     options = dict(_override_default_grpc_options(user_options))
     assert options["grpc.keepalive_time_ms"] == 50000
+    # Verify other defaults are still preserved
+    assert options["grpc.lb_policy_name"] == "round_robin"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/common/grpc_test.py` around lines 11
- 14, The test only checks that an overridden option is applied; add an
assertion that non-overridden defaults remain intact by asserting that
"grpc.lb_policy_name" (or another expected default key) is present in options
and equals the value returned by the default provider—i.e., call the default
generator (e.g., _default_grpc_options() or the same source used by
_override_default_grpc_options) and assert options["grpc.lb_policy_name"] ==
dict(_default_grpc_options())["grpc.lb_policy_name"] inside
test_user_options_override_defaults to verify merge behavior.
python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py (1)

219-220: Avoid hard-coding the current __wrapped__ depth.

These assertions are pinned to the exact decorator layering of get_exporters / get_leases in python/packages/jumpstarter-cli/jumpstarter_cli/get.py, not the CLI behavior. A harmless wrapper reorder will break the test while the commands still call config.list_* correctly. CliRunner().invoke(...) or a small undecorated helper would make this much less brittle.

Also applies to: 237-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py` around lines 219
- 220, The test is brittle because it calls
get_exporters.callback.__wrapped__.__wrapped__ (and similarly get_leases at
lines indicated); replace these direct double-__wrapped__ calls with a stable
invocation: either use click.testing.CliRunner().invoke on the click command
(e.g., CliRunner().invoke(get_exporters, ["--output", "text"]) or
CliRunner().invoke(get_leases, ...)) to exercise the real CLI entrypoint, or
factor out an undecorated helper function that directly calls
config.list_exporters / config.list_leases and call that helper from the test;
update assertions to verify config.list_* calls or command output rather than
relying on decorator depth.
e2e/tests.bats (1)

346-348: Make these pagination tests self-contained.

Both tests assume test-client-oidc already exists, is logged in, and that exporters are already running. That keeps them order-dependent and makes isolated reruns flaky. Please provision and tear down those prerequisites inside the test or a dedicated local helper. Based on learnings: E2E tests in bats should be runnable in isolation. Do not rely on prior suite setup or shared state across tests.

Also applies to: 366-368

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests.bats` around lines 346 - 348, The two pagination tests call
wait_for_exporter and then switch to an existing client via the command jmp
config client use test-client-oidc, making them order-dependent; make each test
self-contained by provisioning and tearing down its prerequisites: create a
dedicated test client (e.g. use jmp config client create ... --oidc
test-client-oidc or equivalent helper), perform an explicit login for that
client (e.g. jmp auth login ...), start exporters inside the test (or call a
local helper like start_exporter) and wait_for_exporter, run the pagination
assertions, then stop exporters (stop_exporter) and delete the test client (or
call a teardown helper) in a trap/cleanup block so the test can run in
isolation; replace direct uses of wait_for_exporter and jmp config client use
test-client-oidc with these setup/teardown steps in both affected tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/tests.bats`:
- Around line 370-386: The current test launches 101 background "jmp admin
create exporter -n ... pagination-exp-${i}" jobs and then counts all YAML
"name:" entries from "jmp get exporters -o yaml", which can false-pass; instead
capture and wait on each background create PID (store PIDs when launching and
wait for them) and scope the get query to only the created exporters (e.g.,
filter by namespace and name pattern or label like "pagination-exp-") and assert
the exact count equals 101 (not >=), and when deleting do the same PID tracking:
record delete PIDs for "jmp admin delete exporter --namespace ...
pagination-exp-${i}" and wait on those PIDs to ensure cleanup completed before
finishing the test.

In `@python/packages/jumpstarter/jumpstarter/config/client.py`:
- Around line 202-226: The list_exporters function currently honors the
page_size for svc.ListExporters but ignores it when calling _collect_all_leases;
change the call leases_response = await self._collect_all_leases(svc) to pass
the page_size parameter (leases_response = await self._collect_all_leases(svc,
page_size=page_size)) so lease enrichment uses the requested page_size, and add
a regression test exercising list_exporters(include_leases=True, page_size=1) to
lock this behavior; ensure any _collect_all_leases signature already accepts
page_size or adjust it accordingly.

---

Nitpick comments:
In `@e2e/tests.bats`:
- Around line 346-348: The two pagination tests call wait_for_exporter and then
switch to an existing client via the command jmp config client use
test-client-oidc, making them order-dependent; make each test self-contained by
provisioning and tearing down its prerequisites: create a dedicated test client
(e.g. use jmp config client create ... --oidc test-client-oidc or equivalent
helper), perform an explicit login for that client (e.g. jmp auth login ...),
start exporters inside the test (or call a local helper like start_exporter) and
wait_for_exporter, run the pagination assertions, then stop exporters
(stop_exporter) and delete the test client (or call a teardown helper) in a
trap/cleanup block so the test can run in isolation; replace direct uses of
wait_for_exporter and jmp config client use test-client-oidc with these
setup/teardown steps in both affected tests.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py`:
- Around line 219-220: The test is brittle because it calls
get_exporters.callback.__wrapped__.__wrapped__ (and similarly get_leases at
lines indicated); replace these direct double-__wrapped__ calls with a stable
invocation: either use click.testing.CliRunner().invoke on the click command
(e.g., CliRunner().invoke(get_exporters, ["--output", "text"]) or
CliRunner().invoke(get_leases, ...)) to exercise the real CLI entrypoint, or
factor out an undecorated helper function that directly calls
config.list_exporters / config.list_leases and call that helper from the test;
update assertions to verify config.list_* calls or command output rather than
relying on decorator depth.

In `@python/packages/jumpstarter/jumpstarter/common/grpc_test.py`:
- Around line 11-14: The test only checks that an overridden option is applied;
add an assertion that non-overridden defaults remain intact by asserting that
"grpc.lb_policy_name" (or another expected default key) is present in options
and equals the value returned by the default provider—i.e., call the default
generator (e.g., _default_grpc_options() or the same source used by
_override_default_grpc_options) and assert options["grpc.lb_policy_name"] ==
dict(_default_grpc_options())["grpc.lb_policy_name"] inside
test_user_options_override_defaults to verify merge behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 537cbd63-648a-4af8-8f80-32db6f01480b

📥 Commits

Reviewing files that changed from the base of the PR and between bb7493b and 1e2cf60.

📒 Files selected for processing (5)
  • e2e/tests.bats
  • python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py
  • python/packages/jumpstarter/jumpstarter/common/grpc_test.py
  • python/packages/jumpstarter/jumpstarter/config/client.py
  • python/packages/jumpstarter/jumpstarter/config/client_config_test.py

Comment thread e2e/tests.bats Outdated
Comment thread python/packages/jumpstarter/jumpstarter/config/client.py Outdated
@raballew raballew force-pushed the 020-grpc-message-size-limit branch from ca699bd to 52ff59b Compare March 19, 2026 12:47
raballew and others added 3 commits March 19, 2026 14:11
The paginated exporter listing test was missing the required -l/--label
option when creating exporters, causing all 101 creations to silently
fail in the background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When list_exporters is called with include_leases=True, the page_size
parameter was not forwarded to _collect_all_leases, always using the
default of 100 regardless of the caller's intent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use --selector to scope the exporter query to only the test-created
exporters and assert exact count (101) instead of >= 101. Track PIDs
from background create commands to ensure all complete before querying.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/config/client.py (1)

181-191: Add a repeated-token guard to prevent unbounded pagination loops.

If the server ever returns the same non-empty next_page_token repeatedly, both loops can run forever. A small guard makes this path safer.

Proposed resiliency patch
 async def _collect_all_leases(self, svc, page_size=100, only_active=True, filter=None):
     from jumpstarter.client.grpc import LeaseList

     all_leases = []
     page_token = None
+    seen_tokens = set()
     while True:
         page = await svc.ListLeases(
             page_size=page_size,
             page_token=page_token,
             filter=filter,
             only_active=only_active,
         )
         all_leases.extend(page.leases)
-        if not page.next_page_token:
+        next_token = page.next_page_token
+        if not next_token:
             break
-        page_token = page.next_page_token
+        if next_token in seen_tokens:
+            raise ConnectionError("ListLeases returned a repeated next_page_token")
+        seen_tokens.add(next_token)
+        page_token = next_token
     return LeaseList(leases=all_leases, next_page_token=None)
     svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
     all_exporters = []
     page_token = None
+    seen_tokens = set()
     while True:
         page = await svc.ListExporters(
             page_size=page_size,
             page_token=page_token,
             filter=filter,
         )
         all_exporters.extend(page.exporters)
-        if not page.next_page_token:
+        next_token = page.next_page_token
+        if not next_token:
             break
-        page_token = page.next_page_token
+        if next_token in seen_tokens:
+            raise ConnectionError("ListExporters returned a repeated next_page_token")
+        seen_tokens.add(next_token)
+        page_token = next_token

Also applies to: 209-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/config/client.py` around lines 181 -
191, The pagination loop using svc.ListLeases (the block that appends to
all_leases and updates page_token) needs a repeated-token guard: track seen
non-empty page tokens (e.g., a set) and if page.next_page_token is non-empty and
already seen, stop and surface an error (or raise/return) to avoid an infinite
loop; apply the same fix to the other identical loop around lines 209-218.
Specifically, update the loop that references page_token, page.next_page_token,
svc.ListLeases, and all_leases to insert the seen-token check before assigning
page_token and before continuing the loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/config/client.py`:
- Around line 181-191: The pagination loop using svc.ListLeases (the block that
appends to all_leases and updates page_token) needs a repeated-token guard:
track seen non-empty page tokens (e.g., a set) and if page.next_page_token is
non-empty and already seen, stop and surface an error (or raise/return) to avoid
an infinite loop; apply the same fix to the other identical loop around lines
209-218. Specifically, update the loop that references page_token,
page.next_page_token, svc.ListLeases, and all_leases to insert the seen-token
check before assigning page_token and before continuing the loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5fa93231-79e3-4279-ba2d-11b6a28c35c0

📥 Commits

Reviewing files that changed from the base of the PR and between ca699bd and 388517c.

📒 Files selected for processing (5)
  • e2e/tests.bats
  • python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py
  • python/packages/jumpstarter/jumpstarter/common/grpc_test.py
  • python/packages/jumpstarter/jumpstarter/config/client.py
  • python/packages/jumpstarter/jumpstarter/config/client_config_test.py
✅ Files skipped from review due to trivial changes (3)
  • python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py
  • python/packages/jumpstarter/jumpstarter/common/grpc_test.py
  • python/packages/jumpstarter/jumpstarter/config/client_config_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/tests.bats

The background process batching caused `wait "$pid"` to fail with
status 127 because intermediate bare `wait` calls already reaped
all child processes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mangelajo mangelajo enabled auto-merge (squash) March 20, 2026 11:17
@mangelajo mangelajo merged commit 0cf0d61 into jumpstarter-dev:main Mar 20, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gRPC max receive message size exceeded when listing all leases

2 participants