Skip to content

Cluster labels: _vocab_lock is held across the whole vocabulary build, blocking the request threads its docstring says it must not #119

Description

@lstein

Summary

_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).

The contradiction, in one file

invokeai/app/services/image_index/image_index_default.py:

def get_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.
    ...
    """
    with self._vocab_lock:          # line 265
def _build_vocab_embeddings(self) -> None:
    ...
    with self._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.
  • Exposure is limited to the first run per embedding model (after fix(image-index): persist the cluster vocabulary cache #111 lands and the cache actually persists — before that, it was every restart).

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_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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions