fix(embedding): exclude provider init from health timeout

This commit is contained in:
jinli.yl 2026-08-21 10:24:15 +08:00
parent ebcb154e37
commit ed67ea80bf
3 changed files with 53 additions and 0 deletions

View file

@ -121,6 +121,15 @@ class BaseAsEmbedding(BaseComponent):
response = await self.model(inputs, **kwargs) # pylint: disable=not-callable
return response.embeddings
def initialize_model(self) -> None:
"""Construct the provider without making a remote request.
Callers that apply their own request timeout can initialize first so
one-time SDK imports and client construction do not consume that
timeout budget. Normal embedding calls remain lazily initialized.
"""
self._ensure_model()
async def _start(self) -> None:
"""Defer provider construction until the first remote embedding call."""
return None

View file

@ -71,6 +71,11 @@ class LocalEmbeddingStore(BaseEmbeddingStore):
async def health_check(self, timeout: float = 5.0) -> bool:
tag = f"[EMBEDDING HEALTH CHECK] name={self.name} workspace_dir={self.workspace_path}"
try:
# Provider construction may synchronously import an SDK and build
# its HTTP client. Keep that one-time work outside the request
# timeout so the full budget applies to the initialized provider
# call instead of being consumed before a request can be sent.
self.as_embedding.initialize_model()
result = await asyncio.wait_for(self.as_embedding(["ping"]), timeout=timeout)
if not result or result[0] is None:
raise RuntimeError("empty embedding")

View file

@ -19,6 +19,9 @@ class FakeAsEmbedding:
dimensions = 2
vector_space_id = "fakespace000"
def initialize_model(self):
"""Mirror the real component's idempotent initialization hook."""
async def __call__(self, texts: list[str], **_kwargs):
return [[1.0] if text == "bad" else [1.0, 0.0] for text in texts]
@ -29,6 +32,9 @@ class BadHealthAsEmbedding:
dimensions = 2
vector_space_id = "fakespace000"
def initialize_model(self):
"""Mirror the real component's idempotent initialization hook."""
async def __call__(self, _texts: list[str], **_kwargs):
return [[1.0]]
@ -147,6 +153,39 @@ def test_health_check_rejects_wrong_dimension():
run(go())
def test_health_check_starts_timeout_after_provider_initialization(monkeypatch):
"""One-time client construction must not consume the request timeout."""
async def go():
events = []
class InitializingAsEmbedding(FakeAsEmbedding):
"""Record initialization and provider-call ordering."""
def initialize_model(self):
events.append("initialized")
async def __call__(self, texts: list[str], **_kwargs):
events.append("remote request")
return [[1.0, 0.0] for _ in texts]
original_wait_for = asyncio.wait_for
async def checked_wait_for(awaitable, timeout):
assert events == ["initialized"]
assert timeout == 5.0
return await original_wait_for(awaitable, timeout)
store = LocalEmbeddingStore(name="t_local_embedding_health_timeout_scope")
store.as_embedding = InitializingAsEmbedding()
monkeypatch.setattr(asyncio, "wait_for", checked_wait_for)
assert await store.health_check(timeout=5.0) is True
assert events == ["initialized", "remote request"]
run(go())
def test_insufficient_quota_waits_sixty_seconds_before_retry(monkeypatch):
"""Quota exhaustion uses the dedicated delay before ReMe retries."""