mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-12 04:01:35 +00:00
* fix: bare except -> except Exception in docling_parser * fix: bare except -> except Exception in methods.py * Address review: bound the spaCy model cache, serialize calls per model Two findings from the Qodo review: - _spacy_model_cache retained every successfully loaded model for the process lifetime — each spaCy Language costs hundreds of MB, so a service varying the model option grew without bound. Now an LRU capped at MAX_SPACY_MODELS_CACHED (4: lg/md/sm + one custom), evicting least-recently-used on insert. - The cached Language was handed to every caller while batch extraction fans out over a ThreadPoolExecutor; spaCy pipelines are not safe to invoke from concurrent threads. Calls now go through per-model locks: spacy_pipeline_guard() yields the pipeline under its own lock (different models still run in parallel), and run_spacy_text() wraps the load/fallback/call chain the entity and relation extractors used to open-code. load_spacy_model() keeps its signature but documents that its result must only be called under the guard. Four regression tests: the bound holds, LRU evicts oldest-not-recently- used, concurrent calls on one model never overlap, and two models can be inside calls at the same time (the lock is per-model, not global). * fix: bare except, bounded LRU spaCy cache, per-model call lock (#1069) Problems fixed -------------- - Replace all 5 bare 'except:' clauses with 'except Exception:' in methods.py (get_nlp_model, extract_relations_similarity fallback) and docling_parser.py (3 sites, including 2 missed by the original PR). - Replace the unbounded dict cache with an OrderedDict LRU bounded at MAX_SPACY_MODELS_CACHED=4. Each spaCy Language is 100-700 MB; the old design pinned every model name ever used for the lifetime of the process. - Fix a TOCTOU race in _load_spacy_entry: the lockless fast-path could call move_to_end() on an entry that had just been evicted by another thread (KeyError). Rewrite as a single critical section: the cache lookup, LRU update, model load, insertion, and eviction all happen under one lock. spacy.load() inside the lock is acceptable because model loads are rare (at most MAX_SPACY_MODELS_CACHED per process). - Add a per-model threading.Lock stored in each cache entry. All calls to nlp(text) go through spacy_pipeline_guard(), which acquires the entry's lock before yielding the Language. This serializes concurrent calls to the same model (spaCy pipelines are not thread-safe) while allowing different models to run in parallel. - Add run_spacy_text() helper to DRY up load-with-fallback-and-logging used in extract_entities_ml, extract_relations_dependency, and extract_relations_similarity. Behavior changes vs main ------------------------ - Pipeline errors during nlp(text) (non-OSError) now log a warning and fall back to pattern extraction, instead of propagating to the caller. This is strictly safer for a service context. - extract_relations_similarity drops an unreachable bare-except path that tried en_core_web_sm when is_package() returned False for all three model names. The is_package() guard is unchanged. - Log messages for model-not-found events now carry a function-scoped label (spaCy NER, spaCy dependency, spaCy similarity) for easier triage in production logs. Tests ----- - Keep and improve four existing cache/lock tests. - Add test_bound_never_exceeded_under_concurrent_load: verifies the single-lock design never exceeds the bound under concurrent pressure (would have caught the old TOCTOU). - Add test_spacy_not_installed_raises_import_error: verifies that spacy=None produces a clear ImportError, not an AttributeError. --------- Co-authored-by: Sameer Kadam <sameerkadam@Mac.lan> Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>