holypug1274 commited on
Commit
099bea1
Β·
1 Parent(s): b921eea

deploy: privacy-sanitizer backend 2026-04-28

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -1
  2. app/main.py +11 -1
  3. app/routers/sanitize.py +43 -6
Dockerfile CHANGED
@@ -27,4 +27,4 @@ ENV PORT=7860
27
  ENV APP_ENV=staging
28
  EXPOSE 7860
29
 
30
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
27
  ENV APP_ENV=staging
28
  EXPOSE 7860
29
 
30
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--forwarded-allow-ips", "*"]
app/main.py CHANGED
@@ -11,7 +11,17 @@ from app.config import settings
11
  from app.routers.sanitize import router as sanitize_router
12
  from app.services.sanitizer import preload_pipeline
13
 
14
- limiter = Limiter(key_func=get_remote_address)
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  @asynccontextmanager
 
11
  from app.routers.sanitize import router as sanitize_router
12
  from app.services.sanitizer import preload_pipeline
13
 
14
+
15
+ def _get_real_ip(request: Request) -> str:
16
+ """Extract the real client IP β€” honours X-Forwarded-For set by HF Spaces / reverse proxy."""
17
+ forwarded_for = request.headers.get("X-Forwarded-For")
18
+ if forwarded_for:
19
+ # X-Forwarded-For: client, proxy1, proxy2 β€” take the leftmost (real client)
20
+ return forwarded_for.split(",")[0].strip()
21
+ return get_remote_address(request)
22
+
23
+
24
+ limiter = Limiter(key_func=_get_real_ip)
25
 
26
 
27
  @asynccontextmanager
app/routers/sanitize.py CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  from fastapi import APIRouter, HTTPException, Request
2
  from slowapi import Limiter
3
  from slowapi.util import get_remote_address
@@ -15,6 +18,17 @@ _ALLOWED_MODEL_IDS: set[str] = {m["id"] for m in AVAILABLE_MODELS}
15
  # Maximum input text length (~10 000 chars β‰ˆ ~2 000 tokens β€” well within BERT limits)
16
  _MAX_TEXT_LENGTH = 10_000
17
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  @router.get("/models", response_model=list[ModelInfo])
20
  @limiter.limit("60/minute")
@@ -37,9 +51,32 @@ async def sanitize(request: Request, body: SanitizeRequest) -> SanitizeResponse:
37
  detail=f"Unknown model '{body.model_name}'. Use GET /api/models for available options.",
38
  )
39
 
40
- try:
41
- return sanitize_text(body.text, model_id=body.model_name)
42
- except RuntimeError as exc:
43
- raise HTTPException(status_code=503, detail=str(exc)) from exc
44
- except Exception as exc:
45
- raise HTTPException(status_code=500, detail="Model inference failed") from exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+
4
  from fastapi import APIRouter, HTTPException, Request
5
  from slowapi import Limiter
6
  from slowapi.util import get_remote_address
 
18
  # Maximum input text length (~10 000 chars β‰ˆ ~2 000 tokens β€” well within BERT limits)
19
  _MAX_TEXT_LENGTH = 10_000
20
 
21
+ # Max simultaneous NER inference jobs β€” prevents CPU saturation on single-process deployments.
22
+ # Requests beyond this limit receive 503 immediately rather than queuing indefinitely.
23
+ _MAX_CONCURRENT_INFERENCES = 2
24
+ _inference_semaphore = asyncio.Semaphore(_MAX_CONCURRENT_INFERENCES)
25
+
26
+ # Dedicated thread pool for CPU-bound inference (avoids blocking the event loop)
27
+ _inference_executor = ThreadPoolExecutor(max_workers=_MAX_CONCURRENT_INFERENCES, thread_name_prefix="ner")
28
+
29
+ # Timeout for a single inference call (seconds) β€” prevents a slow model from blocking forever
30
+ _INFERENCE_TIMEOUT_SECONDS = 60
31
+
32
 
33
  @router.get("/models", response_model=list[ModelInfo])
34
  @limiter.limit("60/minute")
 
51
  detail=f"Unknown model '{body.model_name}'. Use GET /api/models for available options.",
52
  )
53
 
54
+ # Reject immediately if server is already at inference capacity
55
+ if not _inference_semaphore._value: # noqa: SLF001
56
+ raise HTTPException(
57
+ status_code=503,
58
+ detail="Server is busy processing other requests. Please retry in a few seconds.",
59
+ )
60
+
61
+ loop = asyncio.get_running_loop()
62
+ async with _inference_semaphore:
63
+ try:
64
+ result = await asyncio.wait_for(
65
+ loop.run_in_executor(
66
+ _inference_executor,
67
+ sanitize_text,
68
+ body.text,
69
+ body.model_name,
70
+ ),
71
+ timeout=_INFERENCE_TIMEOUT_SECONDS,
72
+ )
73
+ return result
74
+ except asyncio.TimeoutError:
75
+ raise HTTPException(
76
+ status_code=504,
77
+ detail="Model inference timed out. Try a shorter text or switch to BERT-base.",
78
+ )
79
+ except RuntimeError as exc:
80
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
81
+ except Exception as exc:
82
+ raise HTTPException(status_code=500, detail="Model inference failed") from exc