| # Face Intel — Troubleshooting Guide |
|
|
| This guide covers common errors, their causes, and how to fix them. |
| Organized by symptom — find your error message or behavior below. |
|
|
| > **First steps for any issue:** |
| > 1. Check `/health/providers` — are circuits open? Are providers |
| > `not_configured`? |
| > 2. Check `/stats` — are failures climbing? Are cache hits 0? |
| > 3. Check the structured logs — every line carries `eid`, `pid`, |
| > `retry`, `status` for tracing. |
| > 4. Check `/providers` — the `errors` field shows manifest |
| > discovery failures. |
| |
| --- |
| |
| ## Table of Contents |
| |
| 1. [Startup Failures](#1-startup-failures) |
| 2. [Provider Not Available / `not_configured`](#2-provider-not-available--not_configured) |
| 3. [Circuit Breaker Open](#3-circuit-breaker-open) |
| 4. [Model Download Failures](#4-model-download-failures) |
| 5. [dlib Compilation Issues](#5-dlib-compilation-issues) |
| 6. [Selenium / Chrome Issues](#6-selenium--chrome-issues) |
| 7. [Cache Issues](#7-cache-issues) |
| 8. [Database Issues](#8-database-issues) |
| 9. [Performance Tuning](#9-performance-tuning) |
| 10. [API / HTTP Errors](#10-api--http-errors) |
| 11. [Image Validation Errors](#11-image-validation-errors) |
| 12. [Logging Issues](#12-logging-issues) |
| 13. [Recovery Procedures](#13-recovery-procedures) |
| |
| --- |
| |
| ## 1. Startup Failures |
| |
| ### Symptom: `ModuleNotFoundError: No module named 'cv2'` |
| |
| **Cause:** OpenCV not installed. |
| |
| **Fix:** |
| |
| ```bash |
| pip install opencv-python==4.9.0.80 |
| ``` |
| |
| If you see `libGL.so.1: cannot open shared object file` on Linux: |
| |
| ```bash |
| # Debian/Ubuntu |
| sudo apt install -y libgl1 libglib2.0-0 |
| |
| # Or install the headless variant (no GUI deps) |
| pip uninstall opencv-python |
| pip install opencv-python-headless==4.9.0.80 |
| ``` |
| |
| ### Symptom: `ModuleNotFoundError: No module named 'pydantic_settings'` |
|
|
| **Cause:** Wrong pydantic version. Face Intel requires pydantic 2.x |
| with the separate `pydantic-settings` package. |
|
|
| **Fix:** |
|
|
| ```bash |
| pip install pydantic==2.6.1 pydantic-settings==2.2.1 |
| ``` |
|
|
| ### Symptom: `ImportError: cannot import name 'X' from 'Y'` during `build_container()` |
| |
| **Cause:** Circular import or a layer violation. The refactor |
| enforces strict one-way dependency direction. |
| |
| **Fix:** |
| |
| 1. Run `python scripts/check_imports.py` to detect cycles. |
| 2. Check the layer the failing module is in — it must only import |
| from layers below it (see [`docs/ARCHITECTURE.md` §1](ARCHITECTURE.md)). |
| 3. If you recently added a provider, ensure it imports only from |
| `models/`, `utils/`, `config/`, `pipeline/`, `providers/base.py`. |
|
|
| ### Symptom: App boots but `/providers` returns an empty list |
|
|
| **Cause:** All providers are disabled in settings, OR none of them |
| match the manifest. |
|
|
| **Fix:** |
|
|
| 1. Check `.env`: |
|
|
| ```bash |
| grep FI_ENABLE .env |
| ``` |
|
|
| 2. Check the loaded settings: |
|
|
| ```bash |
| python -c "from config.settings import settings; print([k for k,v in settings.model_dump().items() if k.startswith('enable_') and v])" |
| ``` |
|
|
| 3. Check `/providers` for `errors`: |
|
|
| ```bash |
| curl -s http://localhost:8000/providers | jq '.errors' |
| ``` |
|
|
| --- |
|
|
| ## 2. Provider Not Available / `not_configured` |
| |
| A provider shows up in `/providers` with `status: "not_configured"` |
| and `available: false`. |
|
|
| ### Cause 1: Optional Python dependency missing |
|
|
| **Symptom:** |
|
|
| ```json |
| { |
| "name": "insightface", |
| "status": "not_configured", |
| "available": false |
| } |
| ``` |
|
|
| `/providers` `errors` field: |
|
|
| ```json |
| { "insightface": "missing dependency: No module named 'insightface'" } |
| ``` |
|
|
| **Fix:** |
|
|
| ```bash |
| pip install insightface onnxruntime |
| # Also set in .env: |
| # FI_ENABLE_INSIGHTFACE=true |
| ``` |
|
|
| Restart and verify: |
|
|
| ```bash |
| curl -s http://localhost:8000/providers/insightface | jq '.available' |
| # → true |
| ``` |
|
|
| ### Cause 2: API key not set |
|
|
| **Symptom:** Provider enabled in config but `is_available()` returns |
| `False` because the key is empty. |
|
|
| ```bash |
| curl -s http://localhost:8000/providers/serpapi | jq '.' |
| # → {"name": "serpapi", "status": "not_configured", "available": false, ...} |
| ``` |
|
|
| **Fix:** |
|
|
| 1. Edit `.env`: |
|
|
| ```env |
| FI_ENABLE_SERPAPI=true |
| FI_SERPAPI_KEY=your_actual_key_here |
| ``` |
|
|
| 2. Restart. |
|
|
| 3. Verify: |
|
|
| ```bash |
| curl -s http://localhost:8000/providers/serpapi | jq '.available' |
| # → true |
| ``` |
|
|
| See [`docs/CONFIGURATION.md` §17](CONFIGURATION.md#17-how-to-configure-api-keys) |
| for the full key-setup workflow for each paid provider. |
|
|
| ### Cause 3: Provider's `is_available()` raised an exception |
| |
| **Symptom:** Provider shows `not_configured` but no manifest error is |
| recorded. The registry's `info()` method catches the exception and |
| returns `available=False`. |
|
|
| **Fix:** Add logging to your provider's `is_available()`: |
|
|
| ```python |
| def is_available(self) -> bool: |
| try: |
| return self._net is not None |
| except Exception as e: |
| logger.warning(f"is_available() raised: {e}") |
| return False |
| ``` |
|
|
| ### Cause 4: Provider class name doesn't match manifest |
|
|
| **Symptom:** Manifest entry says `class_name="MyProvider"` but the |
| file declares `class MyProviderV2`. The registry will fail to |
| instantiate. |
|
|
| **Fix:** Match the class name exactly. See |
| [`docs/PROVIDERS.md` §7](PROVIDERS.md#7-manifest-entry-format). |
|
|
| ### Cause 5: Provider constructor raised |
|
|
| **Symptom:** Manifest error like `"init error: RuntimeError(...)"`. |
|
|
| **Fix:** Check the constructor. Provider constructors must not raise |
| for missing optional deps — they should catch and store the error, |
| then return `False` from `is_available()`: |
|
|
| ```python |
| def __init__(self, settings): |
| super().__init__(settings=settings) |
| self._init_error = None |
| try: |
| self._model = load_model() |
| except Exception as e: |
| self._init_error = str(e) |
| ``` |
|
|
| See the `dnn` provider |
| ([`providers/detection/dnn.py`](../providers/detection/dnn.py)) |
| for the canonical pattern. |
|
|
| --- |
|
|
| ## 3. Circuit Breaker Open |
|
|
| ### Symptom |
|
|
| A provider that was previously working is now skipped — `providers_invoked` |
| no longer includes it, and `/health/providers` shows `circuit_open: true`: |
|
|
| ```json |
| { |
| "name": "serpapi", |
| "healthy": false, |
| "consecutive_failures": 5, |
| "circuit_open": true, |
| "avg_latency_ms": 0.0 |
| } |
| ``` |
|
|
| ### How the circuit breaker works |
|
|
| See [`docs/CONFIGURATION.md` §11](CONFIGURATION.md#11-health--circuit-breaker). |
| Briefly: |
|
|
| 1. Provider fails → `consecutive_failures += 1`. |
| 2. When `consecutive_failures >= circuit_breaker_failure_threshold` |
| (default 5), the circuit **opens**. |
| 3. After `circuit_breaker_recovery_seconds` (default 120s), the |
| circuit transitions to **half-open**: the next call is allowed. |
| 4. Success → circuit closes. Failure → circuit re-opens. |
|
|
| ### Cause 1: Genuine provider failure |
|
|
| **Diagnosis:** |
|
|
| ```bash |
| curl -s http://localhost:8000/stats | \ |
| jq '.counters | to_entries | map(select(.key | startswith("failures.")))' |
| ``` |
|
|
| If `failures.<provider>` is climbing, the provider is genuinely |
| failing. Check its logs: |
|
|
| ```bash |
| # Tail logs filtered to the provider |
| tail -f /var/log/face-intel/app.log | grep "pid=<provider_name>" |
| ``` |
|
|
| Common root causes: |
|
|
| - API key expired / revoked. |
| - Rate limited by upstream (429). |
| - Network partition. |
| - Upstream service is down. |
|
|
| **Fix:** Resolve the underlying issue. The circuit will close |
| automatically on the next successful call after the recovery window. |
|
|
| ### Cause 2: Misconfigured retry policy |
|
|
| If `retry_max_attempts` is too low and the provider has transient |
| failures, you can hit the failure threshold quickly. |
|
|
| **Fix:** Tune retry: |
|
|
| ```env |
| FI_RETRY_MAX_ATTEMPTS=5 |
| FI_RETRY_INITIAL_BACKOFF_SECONDS=1.0 |
| FI_RETRY_MAX_BACKOFF_SECONDS=30.0 |
| FI_CIRCUIT_BREAKER_FAILURE_THRESHOLD=10 |
| FI_CIRCUIT_BREAKER_RECOVERY_SECONDS=300 |
| ``` |
|
|
| ### Cause 3: Provider timeout too aggressive |
|
|
| If `orchestrator_timeout_seconds=10` but the provider takes 15s on |
| average, every call times out → circuit opens. |
|
|
| **Fix:** |
|
|
| ```env |
| FI_ORCHESTRATOR_TIMEOUT_SECONDS=60 |
| ``` |
|
|
| ### Manual reset |
|
|
| There's no API endpoint to manually close a circuit. Two options: |
|
|
| 1. **Wait** — the circuit auto-transitions to half-open after |
| `circuit_breaker_recovery_seconds`. |
| 2. **Restart** — restart the process. The metrics are in-memory only; |
| they reset on restart (the circuit starts closed). |
|
|
| For a programmatic reset (e.g. for testing), use the metrics facade |
| directly: |
|
|
| ```python |
| container.metrics.health.reset() |
| ``` |
|
|
| ### Verifying recovery |
|
|
| ```bash |
| # Trigger a single call to the provider |
| curl -X POST http://localhost:8000/faces/detect \ |
| -H "Content-Type: application/json" \ |
| -d '{"image_url": "https://example.com/face.jpg", "providers": ["<name>"]}' |
| |
| # Check circuit state |
| curl -s http://localhost:8000/health/providers | \ |
| jq '.providers[] | select(.name == "<name>") | .circuit_open' |
| ``` |
|
|
| --- |
|
|
| ## 4. Model Download Failures |
|
|
| ### Affected providers |
|
|
| - `dnn` — downloads `deploy.prototxt` (~28 KB) and |
| `res10_300x300_ssd_iter_140000.caffemodel` (~10.7 MB) on first |
| use to `data/models/`. |
| - `insightface` — downloads model packs (~100-550 MB) on first use. |
| - `deepface` — downloads backend weights on first use. |
|
|
| ### Symptom: `urllib.error.URLError: <urlopen error ...>` |
|
|
| **Cause:** No internet access from the host, or GitHub raw content |
| is blocked. |
|
|
| **Fix — pre-download the DNN model files:** |
|
|
| ```bash |
| mkdir -p data/models |
| curl -L -o data/models/deploy.prototxt \ |
| https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/deploy.prototxt |
| curl -L -o data/models/res10_300x300_ssd_iter_140000.caffemodel \ |
| https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel |
| |
| # Verify checksums (sha256) |
| sha256sum data/models/* |
| ``` |
|
|
| Then restart. The provider will see the files already exist and skip |
| the download. |
|
|
| ### Symptom: Model file is corrupt (cv2.dnn.readNetFromCaffe fails) |
|
|
| **Cause:** Partial download (e.g. network interrupted). |
|
|
| **Fix:** |
|
|
| ```bash |
| rm data/models/deploy.prototxt data/models/res10_300x300_ssd_iter_140000.caffemodel |
| # Restart — provider will re-download |
| ``` |
|
|
| ### Symptom: Downloads work but model fails to load |
|
|
| ```python |
| cv2.error: OpenCV(4.9.0) /io/opencv/modules/dnn/src/caffe/caffe_importer.cpp... |
| ``` |
|
|
| **Cause:** Corrupt file or OpenCV version mismatch. |
|
|
| **Fix:** |
|
|
| 1. Re-download (see above). |
| 2. Verify OpenCV version: `python -c "import cv2; print(cv2.__version__)"`. |
| Must be `4.9.0.80` per `requirements.txt`. |
| 3. Try `opencv-python-headless` if GUI deps cause issues. |
|
|
| ### Symptom: InsightFace model pack download is very slow |
|
|
| **Cause:** InsightFace downloads from AWS S3 in the US-east region. |
|
|
| **Fix:** Pre-download on a build machine and bake into your Docker |
| image: |
|
|
| ```dockerfile |
| RUN python -c "from insightface.app import FaceAnalysis; FaceAnalysis(name='buffalo_l').prepare(ctx_id=-1)" |
| ``` |
|
|
| This caches the model in `~/.insightface/models/`. Copy that |
| directory into the production image. |
|
|
| ### Air-gapped deployment |
|
|
| For hosts with zero internet access: |
|
|
| 1. On a build machine with internet, run the app once to trigger |
| all model downloads. |
| 2. Tar up `data/models/` and `~/.insightface/` (or wherever the |
| optional libs cache models). |
| 3. Copy to the production host at the same paths. |
| 4. Restart — providers will find the models locally. |
|
|
| --- |
|
|
| ## 5. dlib Compilation Issues |
|
|
| `dlib==19.24.2` does not ship prebuilt wheels for all platforms — |
| on Linux you'll often need to compile from source. |
|
|
| ### Symptom: `error: command 'gcc' failed` during `pip install dlib` |
|
|
| **Cause:** Missing build tools. |
|
|
| **Fix (Debian/Ubuntu):** |
|
|
| ```bash |
| sudo apt install -y build-essential cmake python3-dev |
| pip install dlib==19.24.2 |
| ``` |
|
|
| **Fix (Alpine):** |
|
|
| ```bash |
| apk add --no-cache build-base cmake linux-headers |
| pip install dlib==19.24.2 |
| ``` |
|
|
| **Fix (macOS):** |
|
|
| ```bash |
| xcode-select --install |
| brew install cmake |
| pip install dlib==19.24.2 |
| ``` |
|
|
| ### Symptom: Compilation hangs or runs out of memory |
|
|
| **Cause:** dlib compiles with all cores by default; on small VMs |
| this OOMs. |
|
|
| **Fix:** Limit parallelism: |
|
|
| ```bash |
| pip install --no-build-isolation dlib==19.24.2 \ |
| --config-settings cmake.define.BUILD_SHARED_LIBS=OFF \ |
| -j 2 |
| ``` |
|
|
| Or use a machine with ≥4 GB RAM for the build step. |
|
|
| ### Symptom: `ModuleNotFoundError: No module named 'dlib'` after install |
|
|
| **Cause:** Wrong Python interpreter — dlib installed in a different |
| venv than the one running Face Intel. |
|
|
| **Fix:** |
|
|
| ```bash |
| which python # should be your venv python |
| python -m pip install dlib==19.24.2 |
| python -c "import dlib; print(dlib.__version__)" |
| ``` |
|
|
| ### Symptom: `face_recognition` provider crashes with `Illegal instruction` |
| |
| **Cause:** dlib compiled with AVX instructions but CPU doesn't |
| support them (common on older Xeons / VPS hosts). |
| |
| **Fix:** Recompile dlib without AVX: |
| |
| ```bash |
| pip uninstall dlib |
| git clone https://github.com/davisking/dlib.git |
| cd dlib |
| mkdir build && cd build |
| cmake .. -DUSE_AVX_INSTRUCTIONS=OFF -DCMAKE_BUILD_TYPE=Release |
| make -j 4 |
| cd .. |
| python setup.py install |
| ``` |
| |
| ### Alternative: skip dlib entirely |
| |
| If you don't need recognition: |
| |
| ```env |
| FI_ENABLE_FACE_RECOGNITION=false |
| ``` |
| |
| Then `pip uninstall dlib face-recognition` to remove the broken |
| install. The other 23 providers continue to work. |
| |
| --- |
| |
| ## 6. Selenium / Chrome Issues |
| |
| Affects: `selenium` (scraper), `google_lens` (reverse search). |
| |
| ### Symptom: `selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary` |
| |
| **Cause:** Chrome/Chromium not installed. |
| |
| **Fix (Debian/Ubuntu):** |
| |
| ```bash |
| # Stable Chrome |
| wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb |
| sudo apt install -y ./google-chrome-stable_current_amd64.deb |
|
|
| # Or Chromium |
| sudo apt install -y chromium-browser |
| ``` |
| |
| **Fix (macOS):** |
| |
| ```bash |
| brew install --cask google-chrome |
| ``` |
| |
| **Fix (Docker):** Add to Dockerfile: |
| |
| ```dockerfile |
| RUN apt-get update && apt-get install -y chromium |
| ENV CHROME_BIN=/usr/bin/chromium |
| ``` |
| |
| ### Symptom: `selenium.common.exceptions.SessionNotCreatedException: Message: session not created: Chrome version must be between ...` |
| |
| **Cause:** `webdriver-manager` downloaded a ChromeDriver version |
| that doesn't match your installed Chrome. |
| |
| **Fix:** |
| |
| ```bash |
| google-chrome --version # note the major version |
| # Then ensure chromedriver matches |
| pip install --upgrade webdriver-manager |
| ``` |
| |
| Or use Chrome for Testing with pinned versions: |
| |
| ```bash |
| # Install Chrome for Testing 121 |
| # https://googlechromelabs.github.io/chrome-for-testing/ |
| ``` |
| |
| ### Symptom: Selenium tests hang indefinitely |
| |
| **Cause:** `selenium_implicit_wait` is too high, or Chrome is |
| waiting for a never-resolving resource. |
| |
| **Fix:** |
| |
| 1. Lower implicit wait: |
| |
| ```env |
| FI_SELENIUM_IMPLICIT_WAIT=5 |
| ``` |
| |
| 2. Reduce scroll iterations: |
| |
| ```env |
| FI_SELENIUM_SCROLL_ITERATIONS=2 |
| ``` |
| |
| 3. Set a global scrape timeout: |
| |
| ```env |
| FI_SCRAPE_TIMEOUT=15 |
| ``` |
| |
| 4. For debugging, run non-headless to see what's happening: |
| |
| ```env |
| FI_SELENIUM_HEADLESS=false |
| ``` |
| |
| ### Symptom: Headless Chrome returns empty pages |
| |
| **Cause:** Some sites detect headless Chrome and serve different |
| content. Or JavaScript hasn't finished rendering. |
| |
| **Fix:** |
| |
| 1. Increase scroll iterations: |
| |
| ```env |
| FI_SELENIUM_SCROLL_ITERATIONS=10 |
| ``` |
| |
| 2. Use a more realistic User-Agent: |
| |
| ```env |
| FI_USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" |
| ``` |
| |
| 3. Fall back to the `beautifulsoup` scraper (static HTML) for sites |
| that don't require JS: |
| |
| ```env |
| FI_ENABLE_SELENIUM_SCRAPER=false |
| FI_ENABLE_BEAUTIFULSOUP_SCRAPER=true |
| ``` |
| |
| ### Symptom: Chrome crashes with `--no-sandbox` warning in containers |
| |
| **Cause:** Running Chrome as root in a container requires |
| `--no-sandbox`. Selenium providers should set this automatically; |
| if not, set in your Chrome options. |
| |
| **Fix:** Make sure your selenium provider uses: |
| |
| ```python |
| from selenium.webdriver.chrome.options import Options |
| opts = Options() |
| opts.add_argument("--no-sandbox") |
| opts.add_argument("--disable-dev-shm-usage") # critical in containers |
| opts.add_argument("--headless=new") |
| ``` |
| |
| The `--disable-dev-shm-usage` flag tells Chrome to use `/tmp` |
| instead of `/dev/shm` (which is too small in default containers). |
| |
| --- |
| |
| ## 7. Cache Issues |
| |
| ### Symptom: Cache hit ratio is always 0 |
| |
| **Cause:** Cache is disabled, or every request uses a different |
| image (cache key is `provider:image_hash`). |
|
|
| **Diagnosis:** |
|
|
| ```bash |
| curl -s http://localhost:8000/cache | jq |
| ``` |
|
|
| ```json |
| { |
| "entries": 0, |
| "max_entries": 1000, |
| "ttl_seconds": 3600, |
| "hits": 0, |
| "misses": 24, |
| "hit_ratio": 0.0, |
| "evictions": 0 |
| } |
| ``` |
|
|
| **Fix:** |
|
|
| 1. Ensure cache is enabled: |
|
|
| ```env |
| FI_CACHE_ENABLED=true |
| ``` |
|
|
| 2. Verify you're hitting the same image. The cache key is |
| `f"{provider_name}:{image_hash}"` where `image_hash` is the |
| SHA-256 of the preprocessed image. Slight image differences (URL |
| vs base64, re-encoded JPEGs) produce different hashes. |
|
|
| 3. If you're load-testing with random images, expect 0% hit ratio. |
|
|
| ### Symptom: Cache evictions are high |
|
|
| **Cause:** `cache_max_entries` is too small for the working set. |
|
|
| **Diagnosis:** |
|
|
| ```bash |
| curl -s http://localhost:8000/cache | jq '.evictions' |
| ``` |
|
|
| **Fix:** |
|
|
| ```env |
| FI_CACHE_MAX_ENTRIES=10000 |
| ``` |
|
|
| ### Symptom: Same image returns different results across requests |
|
|
| **Cause:** Cache was cleared, or the TTL expired. |
|
|
| **Diagnosis:** |
|
|
| 1. Check `GET /cache` for `cleared` events — but actually the cache |
| doesn't expose clear count via API. Check the structured logs |
| for `cache clear` events. |
| 2. Check `cache_ttl_seconds` — if it's very short, entries expire |
| between requests. |
|
|
| **Fix:** |
|
|
| ```env |
| FI_CACHE_TTL_SECONDS=86400 # 24 hours |
| ``` |
|
|
| ### Symptom: Stale data — provider updated but cache still serves old result |
|
|
| **Cause:** Cache TTL hasn't expired. |
|
|
| **Fix:** |
|
|
| 1. Clear the cache: |
|
|
| ```bash |
| curl -X DELETE http://localhost:8000/cache |
| ``` |
|
|
| 2. Or invalidate a specific key (not exposed via API — would need a |
| new endpoint, see [`docs/API_REFERENCE.md` §9](API_REFERENCE.md#9-cache-endpoints)). |
|
|
| 3. For permanent fix, lower the TTL or implement a webhook that |
| clears the cache when the upstream model updates. |
|
|
| ### Symptom: Memory usage grows unboundedly |
|
|
| **Cause:** Each cache entry stores a full `ProviderResult` including |
| the `raw` field, which can be large for image-analysis providers. |
|
|
| **Fix:** |
|
|
| 1. Lower `cache_max_entries`: |
|
|
| ```env |
| FI_CACHE_MAX_ENTRIES=100 |
| ``` |
|
|
| 2. For multi-worker deployments, replace the in-memory cache with |
| Redis (see [`docs/DEPLOYMENT.md` §9](DEPLOYMENT.md#9-reverse-proxy-considerations)). |
|
|
| 3. If you can afford recomputation, disable the cache entirely: |
|
|
| ```env |
| FI_CACHE_ENABLED=false |
| ``` |
|
|
| --- |
|
|
| ## 8. Database Issues |
|
|
| ### Symptom: `sqlite3.OperationalError: database is locked` |
|
|
| **Cause:** SQLite writer contention — multiple workers writing |
| concurrently. |
|
|
| **Fix:** |
|
|
| 1. Run a single uvicorn worker (no `--workers N`). |
| 2. Or shard: different DB paths per worker (each worker serves a |
| subset of requests). |
| 3. Or migrate to Postgres (would require a new `Database` |
| implementation — not currently supported). |
|
|
| ### Symptom: `sqlite3.OperationalError: unable to open database file` |
|
|
| **Cause:** The `FI_DB_PATH` directory doesn't exist or isn't |
| writable. |
|
|
| **Fix:** |
|
|
| ```bash |
| # Check the path |
| python -c "from config.settings import settings; print(settings.db_path)" |
| |
| # Ensure the directory exists |
| mkdir -p $(dirname $(python -c "from config.settings import settings; print(settings.db_path)")) |
| |
| # Check permissions |
| ls -la $(dirname $(python -c "from config.settings import settings; print(settings.db_path)")) |
| ``` |
|
|
| ### Symptom: Jobs disappear from `/jobs` after a restart |
|
|
| **Cause:** Using `:memory:` DB by accident. |
|
|
| **Fix:** Check your `.env`: |
|
|
| ```bash |
| grep FI_DB_PATH .env |
| # Should NOT be: |
| # FI_DB_PATH=:memory: |
| ``` |
|
|
| ### Symptom: `/jobs/{id}` returns 404 even though the job ran |
|
|
| **Cause:** The job was created but persistence failed silently (the |
| service catches the exception and continues). |
|
|
| **Diagnosis:** Check the structured logs for `eid=<your job id>`. |
|
|
| **Fix:** If the DB write is failing, you'll see the error in logs. |
| Common causes: disk full, DB locked (see above), schema mismatch. |
|
|
| ### Symptom: DB file is huge |
|
|
| **Cause:** Old jobs and results accumulated. |
|
|
| **Fix:** |
|
|
| 1. Run cleanup: |
|
|
| ```bash |
| python -c " |
| from config.settings import settings |
| from storage.database import Database |
| db = Database(path=settings.db_path) |
| n = db.cleanup_old_jobs(settings.job_retention_days) |
| print(f'Deleted {n} old jobs') |
| " |
| ``` |
|
|
| 2. Compact the file: |
|
|
| ```bash |
| sqlite3 /var/lib/face-intel/face_intel.db "VACUUM;" |
| ``` |
|
|
| 3. Schedule cleanup as a cron job — see |
| [`docs/DEPLOYMENT.md` §5](DEPLOYMENT.md#5-database-setup). |
|
|
| --- |
|
|
| ## 9. Performance Tuning |
|
|
| ### Symptom: Detection jobs take >500 ms |
|
|
| **Diagnosis:** Check `/stats` for per-provider latency: |
|
|
| ```bash |
| curl -s http://localhost:8000/stats | \ |
| jq '.providers[] | {name, avg_latency_ms, p95_latency_ms}' |
| ``` |
|
|
| **Causes & fixes:** |
|
|
| | Cause | Fix | |
| |---|---| |
| | `dnn` running on CPU | Add CUDA-enabled OpenCV (`opencv-python` with CUDA build), or use `haar` for fast pre-filtering. | |
| | `mtcnn` running on CPU | Switch to `dnn` for speed (MTCNN is more accurate but slower). | |
| | Image is huge (e.g. 4K) | Pre-resize before upload, or lower `pipeline.ImagePreprocessor.max_dim` (currently hardcoded to 1024). | |
| | Many providers invoked | Use the `providers` whitelist to invoke only what you need. | |
| | Orchestrator concurrency too low | Raise `FI_ORCHESTRATOR_MAX_CONCURRENCY=16`. | |
|
|
| ### Symptom: Full-pipeline jobs take >30 s |
|
|
| **Cause:** Six concurrent sub-jobs (detection, recognition, image |
| analysis, metadata, forensics, search) all running serially within |
| `asyncio.gather`. |
|
|
| **Fix:** |
|
|
| 1. Disable providers you don't need. If you're not doing reverse |
| search, disable all `SCRAPING` and `REVERSE_SEARCH` providers: |
|
|
| ```env |
| FI_ENABLE_BEAUTIFULSOUP_SCRAPER=false |
| FI_ENABLE_SELENIUM_SCRAPER=false |
| FI_ENABLE_DUCKDUCKGO_SCRAPER=false |
| FI_ENABLE_GOOGLE_LENS=false |
| ``` |
|
|
| 2. Increase orchestrator concurrency: |
|
|
| ```env |
| FI_ORCHESTRATOR_MAX_CONCURRENCY=32 |
| ``` |
|
|
| 3. Increase job timeout if needed: |
|
|
| ```env |
| FI_JOB_TIMEOUT_SECONDS=600 |
| ``` |
|
|
| ### Symptom: High memory usage |
|
|
| **Causes & fixes:** |
|
|
| | Cause | Fix | |
| |---|---| |
| | Many face crops in memory | Lower `pipeline.ImagePreprocessor.max_dim`. | |
| | Cache storing large raw responses | Lower `FI_CACHE_MAX_ENTRIES`. | |
| | InsightFace / DeepFace loaded models | Disable if not needed. | |
| | Selenium Chrome processes | Disable Selenium if not needed. | |
| | Memory leak in custom provider | Profile with `tracemalloc`. | |
|
|
| ### Symptom: High CPU usage at idle |
|
|
| **Cause:** Background polling. Currently Face Intel has no |
| background tasks, so idle CPU should be ~0%. If you see sustained |
| CPU: |
|
|
| 1. Check for runaway Chrome processes (`ps aux | grep chromium`). |
| 2. Check for stuck asyncio tasks (look in the structured logs for |
| `eid` values that never reach `success` or `failed`). |
| 3. Profile with `py-spy`: |
|
|
| ```bash |
| py-spy top --pid <face-intel-pid> |
| ``` |
|
|
| ### Symptom: Network-bound providers are slow |
|
|
| **Causes & fixes:** |
|
|
| | Cause | Fix | |
| |---|---| |
| | High latency to upstream API | Use a CDN or proxy closer to the upstream. | |
| | Rate-limited by upstream | Lower `reverse_search_max_results` to reduce call size. | |
| | Connection pool exhausted | The shared session uses 10 connections per host. For higher concurrency, modify `utils/http.py::make_session()` to bump `pool_maxsize`. | |
| | DNS lookups slow | Configure a local DNS cache (`systemd-resolved`, `dnsmasq`). | |
|
|
| ### Recommended production settings |
|
|
| ```env |
| FI_ORCHESTRATOR_MAX_CONCURRENCY=16 |
| FI_ORCHESTRATOR_TIMEOUT_SECONDS=60 |
| FI_RETRY_MAX_ATTEMPTS=3 |
| FI_CACHE_ENABLED=true |
| FI_CACHE_TTL_SECONDS=86400 |
| FI_CACHE_MAX_ENTRIES=10000 |
| FI_CIRCUIT_BREAKER_FAILURE_THRESHOLD=5 |
| FI_CIRCUIT_BREAKER_RECOVERY_SECONDS=120 |
| ``` |
|
|
| --- |
|
|
| ## 10. API / HTTP Errors |
|
|
| ### `429 Too Many Requests` |
|
|
| **Cause:** Per-IP rate limit exceeded (default 30/min). |
|
|
| **Fix:** |
|
|
| 1. Raise the limit: |
|
|
| ```env |
| FI_RATE_LIMIT_PER_MINUTE=120 |
| ``` |
|
|
| 2. Or exclude specific IPs (would require a code change to |
| `RateLimitMiddleware`). |
|
|
| 3. For multi-process deployments, the in-memory limiter is per |
| process — each worker allows `rate_limit_per_minute`. Replace |
| with Redis-backed limiter for accurate cross-process limiting. |
|
|
| ### `413 Payload Too Large` |
|
|
| **Cause:** Request body exceeded `FI_MAX_REQUEST_BODY_BYTES` |
| (default 25 MB). |
|
|
| **Fix:** |
|
|
| 1. Compress the image before upload. |
| 2. Or raise the limit: |
|
|
| ```env |
| FI_MAX_REQUEST_BODY_BYTES=52428800 # 50 MB |
| ``` |
|
|
| 3. Also raise `FI_MAX_IMAGE_BYTES` if the decoded image is being |
| rejected (default 20 MB): |
|
|
| ```env |
| FI_MAX_IMAGE_BYTES=41943040 # 40 MB |
| ``` |
|
|
| ### `422 Unprocessable Entity` |
|
|
| **Cause:** Pydantic validation failed on the request body. |
|
|
| **Fix:** Check the response body for the validation error details: |
|
|
| ```bash |
| curl -X POST http://localhost:8000/faces/detect \ |
| -H "Content-Type: application/json" \ |
| -d '{"bad": "request"}' -i |
| ``` |
|
|
| The response will include the exact field that failed: |
|
|
| ```json |
| { |
| "detail": [ |
| { |
| "loc": ["body", "image_url"], |
| "msg": "field required", |
| "type": "value_error.missing" |
| } |
| ] |
| } |
| ``` |
|
|
| ### `500 Internal Server Error` |
|
|
| **Cause:** Unhandled exception. `GlobalExceptionMiddleware` catches |
| it and returns: |
|
|
| ```json |
| { |
| "success": false, |
| "error": "<exception message>", |
| "error_type": "<ExceptionClassName>", |
| "request_id": "abc123def456" |
| } |
| ``` |
|
|
| **Fix:** |
|
|
| 1. Note the `request_id`. |
| 2. Find the matching log entries: |
|
|
| ```bash |
| grep "eid=abc123def456" /var/log/face-intel/app.log |
| ``` |
|
|
| 3. The stack trace will be in the log thanks to `logger.exception()`. |
|
|
| ### `GET /jobs/{id}` returns 404 even though `POST /jobs` returned a job_id |
| |
| **Cause:** Job ID mismatch, or the DB write failed silently. |
| |
| **Fix:** |
| |
| 1. Verify the job ID you're passing (copy-paste from the POST |
| response). |
| 2. Check the structured logs for `eid=<job_id>` to see if the job |
| actually persisted. |
| |
| --- |
| |
| ## 11. Image Validation Errors |
| |
| The `InputValidator` (see [`pipeline/validation.py`](../pipeline/validation.py)) |
| returns `ValidationError` for these cases: |
| |
| ### `"No image input provided."` |
| |
| **Cause:** Neither `image_url` nor `image_base64` was set on the |
| request. |
| |
| **Fix:** Pass at least one: |
| |
| ```json |
| {"image_url": "https://example.com/photo.jpg"} |
| ``` |
| |
| ### `"Unsupported URL scheme: ftp"` |
| |
| **Cause:** Only `http` and `https` schemes are allowed. |
| |
| **Fix:** Use an HTTPS URL. |
| |
| ### `"URL missing host."` |
| |
| **Cause:** Malformed URL like `http:///path`. |
| |
| **Fix:** Use a well-formed URL. |
| |
| ### `"Localhost URLs not permitted."` |
| |
| **Cause:** URL host is `localhost`, `127.0.0.1`, `0.0.0.0`, or |
| `::1`. This is a defense against SSRF attacks. |
| |
| **Fix:** Use a publicly resolvable URL. For testing locally, host |
| the image with a tool like `ngrok` or use base64 encoding. |
| |
| ### `"Invalid base64: ..."` |
| |
| **Cause:** Base64 string couldn't be decoded. |
| |
| **Fix:** Verify the base64 is valid: |
| |
| ```bash |
| echo "your_base64_string" | base64 -d | file - |
| # Should print "JPEG image data, ..." |
| ``` |
| |
| If you're including the `data:image/jpeg;base64,` prefix, that's |
| fine — the validator strips it. |
| |
| ### `"Decoded image exceeds 20971520 bytes"` |
| |
| **Cause:** Image is larger than `FI_MAX_IMAGE_BYTES` (default 20 MB). |
| |
| **Fix:** Compress or resize the image, or raise the limit. |
| |
| ### `"Unrecognized image format (magic bytes mismatch)."` |
| |
| **Cause:** The bytes don't match any recognized image signature |
| (JPEG, PNG, GIF, WEBP, BMP, TIFF). |
| |
| **Fix:** Verify the file is actually an image: |
| |
| ```bash |
| file your_image.jpg |
| # Should print "JPEG image data, ..." |
| ``` |
| |
| Common cause: the URL returned an HTML error page instead of an |
| image. Check the URL in a browser first. |
| |
| --- |
| |
| ## 12. Logging Issues |
| |
| ### Symptom: Logs are too verbose |
| |
| **Fix:** |
| |
| ```env |
| FI_LOG_LEVEL=WARNING |
| ``` |
| |
| Levels: `TRACE` < `DEBUG` < `INFO` < `WARNING` < `ERROR` < |
| `CRITICAL`. Most production setups use `INFO`. |
| |
| ### Symptom: Logs are too quiet (can't see provider invocations) |
| |
| **Fix:** |
| |
| ```env |
| FI_LOG_LEVEL=DEBUG |
| ``` |
| |
| Note: DEBUG will include every cache hit/miss and orchestrator |
| decision. Use `TRACE` only for active debugging. |
| |
| ### Symptom: JSON logs have escaped quotes |
| |
| **Cause:** The JSON format string in `utils/logging.py` uses |
| single-quote wrapping around double-quoted JSON. This is correct |
| loguru behavior — the output is valid JSON, just visually escaped |
| when viewed in some terminals. |
| |
| **Fix:** Pipe through `jq`: |
| |
| ```bash |
| tail -f /var/log/face-intel/app.log | jq . |
| ``` |
| |
| ### Symptom: `eid` and `pid` fields are always `-` |
| |
| **Cause:** The log was produced outside an `execution_context()` |
| block. This is normal for startup/shutdown logs. If you see it for |
| provider invocation logs, the context isn't being propagated — |
| check that your provider is invoked through the orchestrator (which |
| sets the context) rather than directly. |
|
|
| ### Symptom: Logs from `loguru` aren't picked up by systemd journal |
|
|
| **Cause:** loguru writes to `sys.stderr` by default. systemd |
| captures stderr, but only if the unit doesn't redirect it. |
|
|
| **Fix:** Either: |
|
|
| 1. Let systemd capture stderr (default behavior — check with |
| `journalctl -u face-intel -f`). |
| 2. Or have loguru write to a file directly by extending |
| `setup_logging()`: |
|
|
| ```python |
| logger.add("/var/log/face-intel/app.log", format=fmt, level=settings.log_level) |
| ``` |
|
|
| --- |
|
|
| ## 13. Recovery Procedures |
|
|
| ### Blackout recovery: all providers failing |
|
|
| If every provider circuit is open: |
|
|
| 1. Check `/health/providers`: |
|
|
| ```bash |
| curl -s http://localhost:8000/health/providers | \ |
| jq '.providers[] | select(.circuit_open == true) | .name' |
| ``` |
|
|
| 2. Identify the common cause (network down? disk full? CPU |
| pegged?). |
| 3. Fix the underlying issue. |
| 4. Restart the app (resets all circuits). |
|
|
| ### Cache corruption |
|
|
| If the cache is returning bad data: |
|
|
| ```bash |
| curl -X DELETE http://localhost:8000/cache |
| # Returns: {"cleared": <N>} |
| ``` |
|
|
| This clears all entries. The next request to each provider will |
| re-populate the cache. |
|
|
| ### Database corruption |
|
|
| If SQLite reports corruption: |
|
|
| 1. Stop the app. |
| 2. Backup the corrupt file (for forensics): |
|
|
| ```bash |
| cp /var/lib/face-intel/face_intel.db /tmp/face_intel.corrupt.db |
| ``` |
|
|
| 3. Try to recover: |
|
|
| ```bash |
| sqlite3 /var/lib/face-intel/face_intel.db ".recover" > /tmp/recovered.sql |
| sqlite3 /var/lib/face-intel/face_intel.new.db < /tmp/recovered.sql |
| mv /var/lib/face-intel/face_intel.new.db /var/lib/face-intel/face_intel.db |
| ``` |
|
|
| 4. If recovery fails, delete the DB and restart (you lose job |
| history but the app re-creates an empty schema): |
|
|
| ```bash |
| rm /var/lib/face-intel/face_intel.db |
| systemctl restart face-intel |
| ``` |
|
|
| ### Gallery corruption |
|
|
| If `data/gallery/manifest.json` is corrupt: |
|
|
| 1. The `ReferenceStore` constructor catches the JSON decode error |
| and starts fresh with an empty gallery. You'll see a warning |
| log: |
|
|
| ``` |
| Gallery manifest corrupted, starting fresh |
| ``` |
|
|
| 2. To restore, re-add known persons via the service: |
|
|
| ```python |
| from api.container import build_container |
| from config.settings import settings |
| container = build_container(settings) |
| # Add each person back... |
| container.recognition_service.add_known_person("alice", embedding_bytes) |
| ``` |
|
|
| 3. If you have backups of the `.npy` files, restore them to |
| `data/gallery/` and rebuild `manifest.json` manually. |
|
|
| ### Provider stuck in `not_configured` after a fix |
| |
| If you've installed the missing dep or set the API key but the |
| provider still shows `not_configured`: |
|
|
| 1. **Restart the app.** The registry only runs `discover()` at |
| startup — it doesn't re-import providers later. |
| 2. Verify the env var is loaded: |
|
|
| ```bash |
| python -c "from config.settings import settings; print(settings.enable_insightface, settings.insightface_model_pack)" |
| ``` |
|
|
| 3. Check `/providers` `errors` for that provider. |
|
|
| --- |
|
|
| ## Diagnostic Cheat Sheet |
|
|
| | Symptom | First command to run | |
| |---|---| |
| | "Provider X not working" | `curl -s http://localhost:8000/providers/X \| jq` | |
| | "Job failed" | `curl -s http://localhost:8000/jobs/<id>/result \| jq` | |
| | "All providers failing" | `curl -s http://localhost:8000/health/providers \| jq '.providers[] \| select(.circuit_open)'` | |
| | "Slow responses" | `curl -s http://localhost:8000/stats \| jq '.providers[] \| {name, p95_latency_ms}'` | |
| | "Cache not working" | `curl -s http://localhost:8000/cache \| jq` | |
| | "Memory growing" | `ps -o pid,rss,vsz,cmd -p <face-intel-pid>` | |
| | "Disk filling" | `du -sh data/*` | |
| | "Mystery error" | `grep "eid=<request_id>" /var/log/face-intel/app.log` | |
|
|
| --- |
|
|
| ## See Also |
|
|
| - [`docs/CONFIGURATION.md`](CONFIGURATION.md) — full settings table |
| for tuning circuit breaker, cache, retry, timeouts. |
| - [`docs/PROVIDERS.md`](PROVIDERS.md) — provider `is_available()` |
| patterns and error handling. |
| - [`docs/DEPLOYMENT.md`](DEPLOYMENT.md) — operational runbook, |
| backup procedures. |
| - [`docs/API_REFERENCE.md` §5](API_REFERENCE.md#5-error-response-format) |
| — error response envelope. |
| - [`docs/ARCHITECTURE.md` §4.3](ARCHITECTURE.md) — circuit breaker |
| lifecycle diagram. |
|
|