|
9 | 9 | import json |
10 | 10 | import logging |
11 | 11 | import time |
| 12 | +from collections.abc import AsyncIterator |
| 13 | +from contextlib import asynccontextmanager |
12 | 14 | from typing import Any |
13 | 15 |
|
14 | 16 | import regex as regex_module |
@@ -174,10 +176,14 @@ def build_analyzer() -> AnalyzerEngine: |
174 | 176 | logger.addHandler(_log_handler) |
175 | 177 | logger.propagate = False |
176 | 178 |
|
177 | | -logger.info("building analyzer (spacy)") |
178 | | -analyzer = build_analyzer() |
179 | | -batch_analyzer = BatchAnalyzerEngine(analyzer_engine=analyzer) |
180 | | -anonymizer = AnonymizerEngine() |
| 179 | +# Built per worker by `lifespan`, never at import: with --workers N the uvicorn |
| 180 | +# supervisor imports this module too, and a module-level build left it holding an |
| 181 | +# idle copy of every model. |
| 182 | +analyzer: AnalyzerEngine |
| 183 | +batch_analyzer: BatchAnalyzerEngine |
| 184 | +anonymizer: AnonymizerEngine |
| 185 | +NER_ENTITIES: frozenset |
| 186 | +_BLANK_ARTIFACTS: dict |
181 | 187 |
|
182 | 188 | # Every entity the spaCy NER recognizers can actually produce. A request touching |
183 | 189 | # any of these must run spaCy; a request naming only non-NER (regex/checksum) |
@@ -207,30 +213,6 @@ def _producible_ner_entities() -> frozenset: |
207 | 213 | return frozenset(producible) |
208 | 214 |
|
209 | 215 |
|
210 | | -NER_ENTITIES = _SPACY_NER_FLOOR | ( |
211 | | - frozenset( |
212 | | - entity |
213 | | - for recognizer in analyzer.registry.recognizers |
214 | | - if isinstance(recognizer, SpacyRecognizer) |
215 | | - for entity in recognizer.supported_entities |
216 | | - ) |
217 | | - & _producible_ner_entities() |
218 | | -) |
219 | | - |
220 | | -# One blank NlpArtifacts per language, built once at startup. Passing these to |
221 | | -# analyze() skips nlp_engine.process_text (the spaCy tok2vec+ner pass) entirely: |
222 | | -# the pattern recognizers still match on the raw text, SpacyRecognizer finds |
223 | | -# nothing in blank artifacts (and can only be consulted at all for claimed-but- |
224 | | -# unproducible entities like PHONE_NUMBER), and score_threshold is unset so |
225 | | -# detection is identical. |
226 | | -# Only context-based score boosting (which needs real tokens) is unavailable — an |
227 | | -# accepted trade for skipping NER on the hot block-output path. Read-only, so it |
228 | | -# is safe to share across requests and workers. |
229 | | -_BLANK_ARTIFACTS = { |
230 | | - language: analyzer.nlp_engine.process_text("", language) |
231 | | - for language in SUPPORTED_LANGUAGES |
232 | | -} |
233 | | - |
234 | 216 |
|
235 | 217 | def _regex_only(entities: list[str] | None, score_threshold: float | None) -> bool: |
236 | 218 | """True when the spaCy NLP pass can be skipped: the request names entities, none |
@@ -308,7 +290,39 @@ def _analyze_many( |
308 | 290 | ) |
309 | 291 |
|
310 | 292 |
|
311 | | -app = FastAPI(title="Sim Presidio", docs_url=None, redoc_url=None) |
| 293 | +@asynccontextmanager |
| 294 | +async def lifespan(_app: FastAPI) -> AsyncIterator[None]: |
| 295 | + global analyzer, batch_analyzer, anonymizer, NER_ENTITIES, _BLANK_ARTIFACTS |
| 296 | + logger.info("building analyzer (spacy)") |
| 297 | + analyzer = build_analyzer() |
| 298 | + batch_analyzer = BatchAnalyzerEngine(analyzer_engine=analyzer) |
| 299 | + anonymizer = AnonymizerEngine() |
| 300 | + NER_ENTITIES = _SPACY_NER_FLOOR | ( |
| 301 | + frozenset( |
| 302 | + entity |
| 303 | + for recognizer in analyzer.registry.recognizers |
| 304 | + if isinstance(recognizer, SpacyRecognizer) |
| 305 | + for entity in recognizer.supported_entities |
| 306 | + ) |
| 307 | + & _producible_ner_entities() |
| 308 | + ) |
| 309 | + # One blank NlpArtifacts per language, built once at startup. Passing these to |
| 310 | + # analyze() skips nlp_engine.process_text (the spaCy tok2vec+ner pass) entirely: |
| 311 | + # the pattern recognizers still match on the raw text, SpacyRecognizer finds |
| 312 | + # nothing in blank artifacts (and can only be consulted at all for claimed-but- |
| 313 | + # unproducible entities like PHONE_NUMBER), and score_threshold is unset so |
| 314 | + # detection is identical. |
| 315 | + # Only context-based score boosting (which needs real tokens) is unavailable — an |
| 316 | + # accepted trade for skipping NER on the hot block-output path. Read-only, so it |
| 317 | + # is safe to share across requests. |
| 318 | + _BLANK_ARTIFACTS = { |
| 319 | + language: analyzer.nlp_engine.process_text("", language) |
| 320 | + for language in SUPPORTED_LANGUAGES |
| 321 | + } |
| 322 | + yield |
| 323 | + |
| 324 | + |
| 325 | +app = FastAPI(title="Sim Presidio", docs_url=None, redoc_url=None, lifespan=lifespan) |
312 | 326 |
|
313 | 327 |
|
314 | 328 | @app.exception_handler(RequestValidationError) |
|
0 commit comments