You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
_build_vocab_embeddings holds _vocab_lock across its entire body — including the minutes-long ensemble_phrase_embeddings call — and get_vocab_embeddings acquires the same lock. So every /cluster_labels request thread blocks for the full duration of a vocabulary build, which is exactly what get_vocab_embeddings' own docstring says must not happen.
Found during adversarial review of #111 and left out of its scope (that PR is a one-line staging-filename fix).
defget_vocab_embeddings(self) ->tuple[list[str], np.ndarray]:
"""... Callers run on the event loop's shared executor, so blocking there — worse, blocking there while holding `_vocab_lock` — starves every other `asyncio.to_thread` in the app, `/points` and image search included. ... """withself._vocab_lock: # line 265
def_build_vocab_embeddings(self) ->None:
...
withself._vocab_lock: # line 285 — wraps the whole body
...
embeddings=ensemble_phrase_embeddings(self._embed_texts, vocabulary) # minutes
The design intent is right — the build is handed to the index worker so it never runs on a request thread. But the lock still serializes request threads against it, so the starvation the docstring warns about happens anyway, just via Lock.acquire instead of via the build itself.
Measured
A reviewer instrumented this with a 1-second simulated build: get_vocab_embeddings blocked for 1.00 s, then returned the freshly-built cache instead of raising the intended TextSearchUnavailableError("Cluster labels are still being prepared; try again shortly"). With a real first-run build that block is minutes, on a thread from the shared asyncio.to_thread executor.
Impact
/cluster_labels requests hang instead of getting the fast 409 the design intends.
Because the executor pool is shared, exhausting it also delays /points and image search.
Do the expensive work outside the lock: take the lock only to check state and to publish the result, and let the build itself run unlocked, with a separate "build in progress" flag so get_vocab_embeddings can raise its 409 immediately rather than waiting. Care is needed so two builds cannot start concurrently and so _vocab_failure is still recorded exactly once.
Note the lock provides no cross-process protection at all — see #121.
Summary
_build_vocab_embeddingsholds_vocab_lockacross its entire body — including the minutes-longensemble_phrase_embeddingscall — andget_vocab_embeddingsacquires the same lock. So every/cluster_labelsrequest thread blocks for the full duration of a vocabulary build, which is exactly whatget_vocab_embeddings' own docstring says must not happen.Found during adversarial review of #111 and left out of its scope (that PR is a one-line staging-filename fix).
The contradiction, in one file
invokeai/app/services/image_index/image_index_default.py:The design intent is right — the build is handed to the index worker so it never runs on a request thread. But the lock still serializes request threads against it, so the starvation the docstring warns about happens anyway, just via
Lock.acquireinstead of via the build itself.Measured
A reviewer instrumented this with a 1-second simulated build:
get_vocab_embeddingsblocked for 1.00 s, then returned the freshly-built cache instead of raising the intendedTextSearchUnavailableError("Cluster labels are still being prepared; try again shortly"). With a real first-run build that block is minutes, on a thread from the sharedasyncio.to_threadexecutor.Impact
/cluster_labelsrequests hang instead of getting the fast 409 the design intends./pointsand image search.Suggested direction
Do the expensive work outside the lock: take the lock only to check state and to publish the result, and let the build itself run unlocked, with a separate "build in progress" flag so
get_vocab_embeddingscan raise its 409 immediately rather than waiting. Care is needed so two builds cannot start concurrently and so_vocab_failureis still recorded exactly once.Note the lock provides no cross-process protection at all — see #121.