fix(pagination): document list contract and harden SDK pagers (#3279)

* fix(pagination): document list contract and harden SDK pagers

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): bound pager token history

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): preflight token history limits

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): paginate sandbox providers

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): harden TypeScript pager

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): expose provider pagers in SDKs

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* docs(pagination): describe a uniform list contract

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(pagination): use stable provider cursors

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* docs(pagination): clarify mutation semantics

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(cli): expose sandbox provider pagination

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(proto): refresh pagination schema inventory

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

* fix(proto): refresh rebased schema inventory

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Kris Hicks <khicks@nvidia.com>

---------

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Kris Hicks <khicks@nvidia.com>
This commit is contained in:
Gaizka Menendez
2026-09-23 18:51:59 +00:00
committed by GitHub
parent 52aac37866
commit 123d95e2ed
26 changed files with 909 additions and 114 deletions
+31 -3
View File
@@ -37,6 +37,8 @@ _ClientCallDetailsBase = namedtuple(
)
_OAUTH_MAX_RESPONSE_BYTES = 1 << 20
_PAGER_MAX_CONSUMED_TOKENS = 10_000
_PAGER_MAX_CONSUMED_TOKEN_BYTES = 1 << 20
T = TypeVar("T")
@@ -49,20 +51,46 @@ class Page(Generic[T]):
class Pager(Generic[T]):
"""Lazy, single-pass iterator that fetches one RPC page per advance."""
"""Lazy, single-pass iterator over the continuation-token contract.
The repeated-token guard has bounded memory and raises ``SandboxError`` if
the traversal exceeds that guard's token-count or byte budget.
"""
def __init__(self, fetch: Callable[[str], Page[T]], page_token: str = "") -> None:
self._fetch = fetch
self._page_token: str | None = page_token
self._consumed_page_tokens: set[str] = set()
self._consumed_page_token_bytes = 0
def __iter__(self) -> Pager[T]:
return self
def _validate_page_token_budget(self, page_token: str) -> int:
if not page_token:
return 0
token_bytes = len(page_token.encode("utf-8"))
if (
len(self._consumed_page_tokens) >= _PAGER_MAX_CONSUMED_TOKENS
or self._consumed_page_token_bytes + token_bytes
> _PAGER_MAX_CONSUMED_TOKEN_BYTES
):
raise SandboxError("pager continuation token history limit exceeded")
return token_bytes
def __next__(self) -> Page[T]:
if self._page_token is None:
raise StopIteration
page = self._fetch(self._page_token)
self._page_token = page.next_page_token or None
page_token = self._page_token
token_bytes = self._validate_page_token_budget(page_token)
page = self._fetch(page_token)
if page_token:
self._consumed_page_tokens.add(page_token)
self._consumed_page_token_bytes += token_bytes
next_page_token = page.next_page_token
if next_page_token and next_page_token in self._consumed_page_tokens:
raise SandboxError("pager received a repeated continuation token")
self._page_token = next_page_token or None
return page
def all(self) -> builtins.list[T]:
+43
View File
@@ -2652,6 +2652,49 @@ def test_pager_retries_same_token_after_fetch_error() -> None:
assert tokens == ["resume", "resume"]
def test_pager_rejects_a_repeated_continuation_token() -> None:
pager = Pager(
lambda token: Page(items=[1], next_page_token=token),
page_token="resume",
)
with pytest.raises(SandboxError, match="repeated continuation token"):
next(pager)
def test_pager_bounds_consumed_token_count(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sandbox_module, "_PAGER_MAX_CONSUMED_TOKENS", 1)
requests: list[str] = []
pager = Pager(
lambda token: (
requests.append(token),
Page(items=[token], next_page_token="next"),
)[1],
page_token="first",
)
assert next(pager).items == ["first"]
with pytest.raises(SandboxError, match="token history limit exceeded"):
next(pager)
assert requests == ["first"]
def test_pager_bounds_consumed_token_bytes(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sandbox_module, "_PAGER_MAX_CONSUMED_TOKEN_BYTES", 1)
requests: list[str] = []
pager = Pager(
lambda token: (
requests.append(token),
Page(items=[token], next_page_token="next"),
)[1],
page_token="too-large",
)
with pytest.raises(SandboxError, match="token history limit exceeded"):
next(pager)
assert requests == []
def test_list_ids_forwards_label_selector() -> None:
stub = _FakeSandboxStub(listed=[_make_sandbox_proto("sandbox-1", "job-1")])
client = _client_with_fake_stub(stub)