Skip to content

Commit 868ae2f

Browse files
committed
feat(grpc): wire throttle callback from _AdaptiveLimiterRegistry to GrpcChannel
- Add `on_throttle: Callable[[str], None] | None = None` parameter to `GrpcIndex.__init__` and forward it to the Rust-backed `GrpcChannel` constructor. - Update `Pinecone.index(grpc=True)` factory to pass `self._limiter_registry.report_throttled` as the `on_throttle` callback, completing the loop started by DX-0159 (REST->registry). - Add `tests/unit/grpc/test_grpc_limiter_wiring.py` with 4 tests covering: - Default on_throttle=None for direct GrpcIndex construction - Callable forwarding when on_throttle is provided - Pinecone client wires registry callback (verifies __self__ identity) - Direct construction without client does not raise - Update TestIndexFactoryGrpc tests to assert per-kwarg (bound-method identity safe). DX-0160: gRPC throttle events (RESOURCE_EXHAUSTED) now flow into the same per-host _AdaptiveLimiter as REST 429s. Bulk paths read limiter.current_limit() to adjust concurrency.
1 parent 80c7386 commit 868ae2f

4 files changed

Lines changed: 116 additions & 21 deletions

File tree

‎pinecone/_client.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ def index(
394394
host=resolved_host,
395395
api_key=self._config.api_key,
396396
source_tag=self._config.source_tag or None,
397+
on_throttle=self._limiter_registry.report_throttled,
397398
)
398399

399400
from pinecone.index import Index as _Index

‎pinecone/grpc/__init__.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import builtins
66
import logging
77
import os
8-
from collections.abc import Iterator, Mapping, Sequence
8+
from collections.abc import Callable, Iterator, Mapping, Sequence
99
from concurrent.futures import ThreadPoolExecutor
1010
from typing import TYPE_CHECKING, Any
1111

@@ -185,6 +185,7 @@ def __init__(
185185
secure: bool = True,
186186
timeout: float = 20.0,
187187
connect_timeout: float = 1.0,
188+
on_throttle: Callable[[str], None] | None = None,
188189
) -> None:
189190
# Resolve API key: explicit arg > env var
190191
resolved_key = api_key or os.environ.get("PINECONE_API_KEY", "")
@@ -213,6 +214,7 @@ def __init__(
213214
timeout,
214215
connect_timeout,
215216
source_tag=source_tag,
217+
on_throttle=on_throttle,
216218
)
217219

218220
self._executor = ThreadPoolExecutor()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Tests that GrpcIndex wires the throttle callback to the _AdaptiveLimiterRegistry."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from unittest.mock import MagicMock, patch
7+
8+
_MOCK_GRPC_MODULE_PATH = "pinecone._grpc"
9+
10+
11+
def _make_grpc_index_with_mock(
12+
on_throttle: Callable[[str], None] | None = None,
13+
) -> tuple[MagicMock, MagicMock]:
14+
"""Return (mock_channel_class, grpc_index_instance) for inspection."""
15+
from pinecone.grpc import GrpcIndex
16+
17+
mock_channel = MagicMock()
18+
mock_module = MagicMock()
19+
mock_module.GrpcChannel.return_value = mock_channel
20+
with patch.dict("sys.modules", {_MOCK_GRPC_MODULE_PATH: mock_module}):
21+
idx = GrpcIndex(
22+
host="https://test-idx-abc.svc.pinecone.io",
23+
api_key="test-key",
24+
on_throttle=on_throttle,
25+
)
26+
return mock_module.GrpcChannel, idx
27+
28+
29+
def test_grpc_index_construction_passes_on_throttle_none_by_default() -> None:
30+
mock_channel_cls, _ = _make_grpc_index_with_mock(on_throttle=None)
31+
_, kwargs = mock_channel_cls.call_args
32+
assert kwargs.get("on_throttle") is None
33+
34+
35+
def test_grpc_index_construction_passes_on_throttle_callable() -> None:
36+
sentinel: list[str] = []
37+
38+
def callback(host: str) -> None:
39+
sentinel.append(host)
40+
41+
mock_channel_cls, _ = _make_grpc_index_with_mock(on_throttle=callback)
42+
_, kwargs = mock_channel_cls.call_args
43+
assert kwargs.get("on_throttle") is callback
44+
45+
46+
def test_pinecone_client_wires_registry_to_grpc_index() -> None:
47+
"""GrpcIndex constructed via Pinecone.index(grpc=True) receives the registry callback."""
48+
from pinecone import Pinecone
49+
50+
pc = Pinecone(api_key="test-key")
51+
52+
mock_channel = MagicMock()
53+
mock_module = MagicMock()
54+
mock_module.GrpcChannel.return_value = mock_channel
55+
56+
with (
57+
patch.dict("sys.modules", {_MOCK_GRPC_MODULE_PATH: mock_module}),
58+
patch.object(pc, "_resolve_index_host", return_value="test-idx-abc.svc.pinecone.io"),
59+
):
60+
pc.index(host="test-idx-abc.svc.pinecone.io", grpc=True)
61+
62+
_, kwargs = mock_module.GrpcChannel.call_args
63+
on_throttle = kwargs.get("on_throttle")
64+
assert on_throttle is not None, "on_throttle should be wired from the limiter registry"
65+
assert callable(on_throttle)
66+
# Bound methods create a new object on each attribute access, so compare via __func__/__self__
67+
assert on_throttle.__func__ is pc._limiter_registry.report_throttled.__func__
68+
assert on_throttle.__self__ is pc._limiter_registry
69+
70+
71+
def test_grpc_index_direct_construction_on_throttle_none_does_not_raise() -> None:
72+
"""GrpcIndex constructed directly (no parent client) works fine with on_throttle=None."""
73+
from pinecone.grpc import GrpcIndex
74+
75+
mock_channel = MagicMock()
76+
mock_module = MagicMock()
77+
mock_module.GrpcChannel.return_value = mock_channel
78+
with patch.dict("sys.modules", {_MOCK_GRPC_MODULE_PATH: mock_module}):
79+
GrpcIndex(
80+
host="https://direct-construction.svc.pinecone.io",
81+
api_key="test-key",
82+
)
83+
84+
_, kwargs = mock_module.GrpcChannel.call_args
85+
assert kwargs.get("on_throttle") is None

‎tests/unit/test_client.py‎

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -413,11 +413,15 @@ def test_grpc_true_returns_grpc_index(self, mock_grpc_cls: MagicMock) -> None:
413413
result = pc.index(host="foo.svc.pinecone.io", grpc=True)
414414

415415
assert result is mock_grpc_idx
416-
mock_grpc_cls.assert_called_once_with(
417-
host="foo.svc.pinecone.io",
418-
api_key="test-key",
419-
source_tag=None,
420-
)
416+
mock_grpc_cls.assert_called_once()
417+
_, call_kwargs = mock_grpc_cls.call_args
418+
assert call_kwargs["host"] == "foo.svc.pinecone.io"
419+
assert call_kwargs["api_key"] == "test-key"
420+
assert call_kwargs["source_tag"] is None
421+
# on_throttle must be wired to the registry's report_throttled method
422+
on_throttle = call_kwargs["on_throttle"]
423+
assert callable(on_throttle)
424+
assert on_throttle.__self__ is pc._limiter_registry
421425

422426
@patch("pinecone.grpc.GrpcIndex")
423427
def test_grpc_true_passes_source_tag(self, mock_grpc_cls: MagicMock) -> None:
@@ -426,11 +430,12 @@ def test_grpc_true_passes_source_tag(self, mock_grpc_cls: MagicMock) -> None:
426430

427431
pc.index(host="foo.svc.pinecone.io", grpc=True)
428432

429-
mock_grpc_cls.assert_called_once_with(
430-
host="foo.svc.pinecone.io",
431-
api_key="test-key",
432-
source_tag="my_app",
433-
)
433+
mock_grpc_cls.assert_called_once()
434+
_, call_kwargs = mock_grpc_cls.call_args
435+
assert call_kwargs["host"] == "foo.svc.pinecone.io"
436+
assert call_kwargs["api_key"] == "test-key"
437+
assert call_kwargs["source_tag"] == "my_app"
438+
assert callable(call_kwargs["on_throttle"])
434439

435440
@patch("pinecone.grpc.GrpcIndex")
436441
def test_grpc_true_with_name_cached(self, mock_grpc_cls: MagicMock) -> None:
@@ -440,11 +445,12 @@ def test_grpc_true_with_name_cached(self, mock_grpc_cls: MagicMock) -> None:
440445

441446
pc.index(name="my-index", grpc=True)
442447

443-
mock_grpc_cls.assert_called_once_with(
444-
host="cached.host.pinecone.io",
445-
api_key="test-key",
446-
source_tag=None,
447-
)
448+
mock_grpc_cls.assert_called_once()
449+
_, call_kwargs = mock_grpc_cls.call_args
450+
assert call_kwargs["host"] == "cached.host.pinecone.io"
451+
assert call_kwargs["api_key"] == "test-key"
452+
assert call_kwargs["source_tag"] is None
453+
assert callable(call_kwargs["on_throttle"])
448454

449455
@patch("pinecone.grpc.GrpcIndex")
450456
def test_grpc_true_with_name_describe(self, mock_grpc_cls: MagicMock) -> None:
@@ -461,11 +467,12 @@ def test_grpc_true_with_name_describe(self, mock_grpc_cls: MagicMock) -> None:
461467

462468
mock_indexes.describe.assert_called_once_with("my-index")
463469
assert pc._host_cache["my-index"] == "resolved.host.pinecone.io"
464-
mock_grpc_cls.assert_called_once_with(
465-
host="resolved.host.pinecone.io",
466-
api_key="test-key",
467-
source_tag=None,
468-
)
470+
mock_grpc_cls.assert_called_once()
471+
_, call_kwargs = mock_grpc_cls.call_args
472+
assert call_kwargs["host"] == "resolved.host.pinecone.io"
473+
assert call_kwargs["api_key"] == "test-key"
474+
assert call_kwargs["source_tag"] is None
475+
assert callable(call_kwargs["on_throttle"])
469476

470477
@patch("pinecone.index.Index")
471478
def test_grpc_false_default_returns_http_index(self, mock_index_cls: MagicMock) -> None:

0 commit comments

Comments
 (0)