Files
semantica/tests
1914dd508c fix: replace bare except with except Exception in 3 sites (#1069)
* 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>
2026-09-10 13:53:55 +05:30
..
2026-06-16 04:43:45 +05:00