fix(vector): address PR review round 8 — anyio convention + cosine-safe sentinel + dedup get_collection
Reviewer findings (1 blocking + 2 important): - 🔴 Replace `import asyncio` / `await asyncio.sleep(0)` with `import anyio` / `await anyio.sleep(0)` in the four async side-effect helpers (_scroll_raises, _upsert_raises, _get_collection_raises, _create_index). CLAUDE.md mandates anyio for all async operations; conftest pins the backend to asyncio so the asyncio.sleep call worked today, but the inconsistency would surface the moment that pin moves. - 🟡 Replace the sentinel's zero dense vector with a single non-zero element (`[1e-9] + [0.0] * (dimension - 1)`). Cosine distance is mathematically undefined for the zero vector and Qdrant Cloud strict mode rejects zero-vector upserts. The exact value doesn't matter (sentinel never participates in a search — no user_id/doc_id/doc_type payload) but the upsert itself must be valid. - 🟡 Avoid the duplicate `get_collection` round-trip on every restart. `_ensure_payload_indexes` now accepts an optional `existing_schema: dict | None` parameter; when None it fetches collection_info itself (and the get_collection-failure swallow still applies), but `get_qdrant_client` already fetches collection_info for dimension validation in the existing-collection branch — pass `collection_info.payload_schema or {}` through to skip the second call. The new-collection branch passes `existing_schema={}` explicitly since a freshly created collection has no payload schema. The 🟡 deck_card iteration-fallback finding doesn't apply: the `isdigit()` guard at context.py:612 returns early before either the fast-path or the iteration fallback runs, so non-numeric doc_ids cannot reach the inner `c.id == int(doc_id)` comparison. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9720a7e4fe
commit
d390b3a4b8
@@ -14,10 +14,10 @@ run at startup. Producer-side normalization is exercised by the existing
|
||||
scanner tests.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import call
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import pytest
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
@@ -237,7 +237,7 @@ async def test_ensure_payload_indexes_logs_and_returns_when_get_collection_raise
|
||||
|
||||
async def _get_collection_raises(*args, **kwargs):
|
||||
# See _scroll_raises in the backfill section for why this is async.
|
||||
await asyncio.sleep(0)
|
||||
await anyio.sleep(0)
|
||||
raise RuntimeError("connection refused")
|
||||
|
||||
client.get_collection.side_effect = _get_collection_raises
|
||||
@@ -481,11 +481,11 @@ async def test_backfill_logs_and_returns_when_scroll_raises(mocker, caplog):
|
||||
# An async-callable side_effect lets AsyncMock await the coroutine
|
||||
# before the exception propagates; assigning a bare exception class
|
||||
# leaks an un-awaited coroutine and trips RuntimeWarning at gc time.
|
||||
# The `await asyncio.sleep(0)` is a no-op event-loop yield that
|
||||
# The `await anyio.sleep(0)` is a no-op event-loop yield that
|
||||
# satisfies static analysis ("async function uses no async features")
|
||||
# without changing observable behavior.
|
||||
async def _scroll_raises(*args, **kwargs):
|
||||
await asyncio.sleep(0)
|
||||
await anyio.sleep(0)
|
||||
raise RuntimeError("boom")
|
||||
|
||||
client.scroll.side_effect = _scroll_raises
|
||||
@@ -525,7 +525,7 @@ async def test_backfill_logs_warning_when_sentinel_upsert_fails(mocker, caplog):
|
||||
|
||||
async def _upsert_raises(*args, **kwargs):
|
||||
# See _scroll_raises above for why this is async + sleep(0).
|
||||
await asyncio.sleep(0)
|
||||
await anyio.sleep(0)
|
||||
raise RuntimeError("sentinel write blip")
|
||||
|
||||
client.upsert.side_effect = _upsert_raises
|
||||
@@ -602,7 +602,7 @@ async def test_ensure_payload_indexes_summarises_failed_fields(mocker, caplog):
|
||||
|
||||
async def _create_index(*args, **kwargs):
|
||||
# See _scroll_raises above for why this is async + sleep(0).
|
||||
await asyncio.sleep(0)
|
||||
await anyio.sleep(0)
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] != 2:
|
||||
raise _make_unexpected(500, b'{"status":{"error":"boom"}}')
|
||||
|
||||
Reference in New Issue
Block a user