mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
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:
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user