Coverage for src/qdrant_loader/core/text_processing/spacy_model_cache.py: 95%
19 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""Process-wide, thread-safe cache for loaded spaCy models.
3``spacy.load()`` is expensive (disk read + model deserialization, often
4hundreds of milliseconds), and the returned ``Language`` object is safe to
5share for read-only inference across callers. Without this cache, callers
6like ``TextProcessor`` and ``SemanticAnalyzer`` were re-loading a model on
7every construction -- which happens once per document, since chunking
8strategies (and therefore their text processors) are instantiated per
9document. Because document chunking runs on a thread pool, that repeated,
10heavy CPU-bound load also serializes chunk-worker threads on the GIL,
11defeating ``max_chunk_workers`` concurrency.
13The cache is held under a lock only while the first load for a given key is
14in flight, so concurrent callers block and share that one load instead of
15each independently loading the model.
16"""
18import threading
19from collections.abc import Callable
20from typing import Any
22_cache: dict[Any, Any] = {}
23_lock = threading.Lock()
26def get_or_load(cache_key: Any, loader: Callable[[], Any]) -> Any:
27 """Return the cached value for ``cache_key``, loading it via ``loader()`` once.
29 ``loader`` is only ever invoked on a cache miss, and is called by the
30 caller's own module so provider-specific error handling (e.g. an
31 OSError-triggered model download) stays where it already is.
32 """
33 cached = _cache.get(cache_key)
34 if cached is not None:
35 return cached
37 with _lock:
38 # Re-check: another thread may have populated it while we waited.
39 cached = _cache.get(cache_key)
40 if cached is not None:
41 return cached
43 value = loader()
44 _cache[cache_key] = value
45 return value
48def clear() -> None:
49 """Clear all cached models. Intended for test isolation."""
50 with _lock:
51 _cache.clear()