Skip to content

Commit 352fb73

Browse files
committed
fix(indexes): raise IndexTerminatedError for Terminating/Disabled states during polling
## Purpose `poll_index_until_ready` and `async_poll_index_until_ready` only escaped the polling loop on `InitializationFailed`. Indexes entering `Terminating` or `Disabled` states caused the poller to spin until timeout, giving the caller a generic `PineconeTimeoutError` instead of an actionable error. ## Solution Added `IndexTerminatedError(PineconeError)` to `pinecone/errors/exceptions.py` and exported it from `pinecone/errors/__init__.py`. Both sync and async pollers in `pinecone/_internal/indexes_helpers.py` now check for `Terminating`/`Disabled` immediately after the `InitializationFailed` guard and raise `IndexTerminatedError` with the index name and terminal state string. Unit tests cover all three terminal states (plus the ready success path) for both sync and async variants.
1 parent 61ceeb1 commit 352fb73

4 files changed

Lines changed: 107 additions & 3 deletions

File tree

pinecone/_internal/indexes_helpers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,14 @@ def poll_index_until_ready(
321321
322322
Raises:
323323
IndexInitFailedError: If the index enters ``InitializationFailed`` state.
324+
IndexTerminatedError: If the index enters ``Terminating`` or ``Disabled`` state.
324325
PineconeTimeoutError: If *timeout* seconds elapse without becoming ready.
325326
"""
326-
from pinecone.errors.exceptions import IndexInitFailedError, PineconeTimeoutError
327+
from pinecone.errors.exceptions import (
328+
IndexInitFailedError,
329+
IndexTerminatedError,
330+
PineconeTimeoutError,
331+
)
327332

328333
start = time.monotonic()
329334
while True:
@@ -332,6 +337,8 @@ def poll_index_until_ready(
332337
return idx
333338
if idx.status.state == "InitializationFailed":
334339
raise IndexInitFailedError(name)
340+
if idx.status.state in ("Terminating", "Disabled"):
341+
raise IndexTerminatedError(name, idx.status.state)
335342
if timeout is not None:
336343
elapsed = time.monotonic() - start
337344
if elapsed >= timeout:
@@ -359,9 +366,14 @@ async def async_poll_index_until_ready(
359366
360367
Raises:
361368
IndexInitFailedError: If the index enters ``InitializationFailed`` state.
369+
IndexTerminatedError: If the index enters ``Terminating`` or ``Disabled`` state.
362370
PineconeTimeoutError: If *timeout* seconds elapse without becoming ready.
363371
"""
364-
from pinecone.errors.exceptions import IndexInitFailedError, PineconeTimeoutError
372+
from pinecone.errors.exceptions import (
373+
IndexInitFailedError,
374+
IndexTerminatedError,
375+
PineconeTimeoutError,
376+
)
365377

366378
start = time.monotonic()
367379
while True:
@@ -370,6 +382,8 @@ async def async_poll_index_until_ready(
370382
return idx
371383
if idx.status.state == "InitializationFailed":
372384
raise IndexInitFailedError(name)
385+
if idx.status.state in ("Terminating", "Disabled"):
386+
raise IndexTerminatedError(name, idx.status.state)
373387
if timeout is not None:
374388
elapsed = time.monotonic() - start
375389
if elapsed >= timeout:

pinecone/errors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
ConflictError,
88
ForbiddenError,
99
IndexInitFailedError,
10+
IndexTerminatedError,
1011
NotFoundError,
1112
PineconeConnectionError,
1213
PineconeError,
@@ -24,6 +25,7 @@
2425
"ConflictError",
2526
"ForbiddenError",
2627
"IndexInitFailedError",
28+
"IndexTerminatedError",
2729
"NotFoundError",
2830
"PineconeConnectionError",
2931
"PineconeError",

pinecone/errors/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,21 @@ def __init__(self, index_name: str) -> None:
210210
self.index_name = index_name
211211

212212

213+
class IndexTerminatedError(PineconeError):
214+
"""Raised when polling an index that has entered a terminal non-init state.
215+
216+
Terminal states include ``Terminating`` and ``Disabled``.
217+
"""
218+
219+
def __init__(self, name: str, state: str) -> None:
220+
super().__init__(
221+
f"Index '{name}' entered terminal state '{state}'. "
222+
f"Check status with pc.describe_index(name='{name}')."
223+
)
224+
self.name = name
225+
self.state = state
226+
227+
213228
class PineconeTimeoutError(PineconeError, TimeoutError):
214229
"""Raised when an operation exceeds its timeout.
215230

tests/unit/test_indexes_helpers.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
from __future__ import annotations
44

5+
from unittest.mock import AsyncMock, MagicMock
6+
57
import pytest
68

79
from pinecone._internal.indexes_helpers import (
810
_normalize_schema,
11+
async_poll_index_until_ready,
912
build_byoc_body,
1013
build_create_body,
1114
build_integrated_body,
15+
poll_index_until_ready,
1216
validate_read_capacity,
1317
)
14-
from pinecone.errors.exceptions import ValidationError
18+
from pinecone.errors.exceptions import IndexInitFailedError, IndexTerminatedError, ValidationError
19+
from pinecone.models.indexes.index import IndexModel, IndexSpec, IndexStatus, ServerlessSpecInfo
1520
from pinecone.models.indexes.specs import ByocSpec, EmbedConfig, IntegratedSpec, ServerlessSpec
1621

1722

@@ -358,3 +363,71 @@ def test_build_integrated_body_method_schema_bare_input_gets_wrapped() -> None:
358363
schema={"genre": {"filterable": True}},
359364
)
360365
assert body["schema"] == {"fields": {"genre": {"filterable": True}}}
366+
367+
368+
# poll_index_until_ready terminal-state tests
369+
370+
371+
def _make_index(state: str, ready: bool = False) -> IndexModel:
372+
return IndexModel(
373+
name="test-index",
374+
dimension=1536,
375+
metric="cosine",
376+
host="test-index.svc.pinecone.io",
377+
spec=IndexSpec(serverless=ServerlessSpecInfo(cloud="aws", region="us-east-1")),
378+
status=IndexStatus(ready=ready, state=state),
379+
deletion_protection="disabled",
380+
tags=None,
381+
)
382+
383+
384+
def test_poll_index_until_ready_raises_on_terminating() -> None:
385+
describe_fn = MagicMock(return_value=_make_index("Terminating"))
386+
with pytest.raises(IndexTerminatedError) as exc_info:
387+
poll_index_until_ready(describe_fn, "test-index", timeout=10)
388+
assert exc_info.value.state == "Terminating"
389+
assert exc_info.value.name == "test-index"
390+
391+
392+
def test_poll_index_until_ready_raises_on_disabled() -> None:
393+
describe_fn = MagicMock(return_value=_make_index("Disabled"))
394+
with pytest.raises(IndexTerminatedError) as exc_info:
395+
poll_index_until_ready(describe_fn, "test-index", timeout=10)
396+
assert exc_info.value.state == "Disabled"
397+
assert exc_info.value.name == "test-index"
398+
399+
400+
def test_poll_index_until_ready_still_raises_on_initialization_failed() -> None:
401+
describe_fn = MagicMock(return_value=_make_index("InitializationFailed"))
402+
with pytest.raises(IndexInitFailedError):
403+
poll_index_until_ready(describe_fn, "test-index", timeout=10)
404+
405+
406+
def test_poll_index_until_ready_returns_on_ready(monkeypatch: pytest.MonkeyPatch) -> None:
407+
monkeypatch.setattr("pinecone._internal.indexes_helpers.time.sleep", lambda *_: None)
408+
ready_index = _make_index("Ready", ready=True)
409+
describe_fn = MagicMock(return_value=ready_index)
410+
result = poll_index_until_ready(describe_fn, "test-index", timeout=10)
411+
assert result.status.ready is True
412+
413+
414+
async def test_async_poll_index_until_ready_raises_on_terminating() -> None:
415+
describe_fn = AsyncMock(return_value=_make_index("Terminating"))
416+
with pytest.raises(IndexTerminatedError) as exc_info:
417+
await async_poll_index_until_ready(describe_fn, "test-index", timeout=10)
418+
assert exc_info.value.state == "Terminating"
419+
assert exc_info.value.name == "test-index"
420+
421+
422+
async def test_async_poll_index_until_ready_raises_on_disabled() -> None:
423+
describe_fn = AsyncMock(return_value=_make_index("Disabled"))
424+
with pytest.raises(IndexTerminatedError) as exc_info:
425+
await async_poll_index_until_ready(describe_fn, "test-index", timeout=10)
426+
assert exc_info.value.state == "Disabled"
427+
assert exc_info.value.name == "test-index"
428+
429+
430+
async def test_async_poll_index_until_ready_still_raises_on_initialization_failed() -> None:
431+
describe_fn = AsyncMock(return_value=_make_index("InitializationFailed"))
432+
with pytest.raises(IndexInitFailedError):
433+
await async_poll_index_until_ready(describe_fn, "test-index", timeout=10)

0 commit comments

Comments
 (0)