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

1"""Process-wide, thread-safe cache for loaded spaCy models. 

2 

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. 

12 

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""" 

17 

18import threading 

19from collections.abc import Callable 

20from typing import Any 

21 

22_cache: dict[Any, Any] = {} 

23_lock = threading.Lock() 

24 

25 

26def get_or_load(cache_key: Any, loader: Callable[[], Any]) -> Any: 

27 """Return the cached value for ``cache_key``, loading it via ``loader()`` once. 

28 

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 

36 

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 

42 

43 value = loader() 

44 _cache[cache_key] = value 

45 return value 

46 

47 

48def clear() -> None: 

49 """Clear all cached models. Intended for test isolation.""" 

50 with _lock: 

51 _cache.clear()