Skip to content

feat(inference): extract geospatial workloads from backend - #673

Draft
frgfm wants to merge 5 commits into
mainfrom
codex/extract-geospatial-inference
Draft

feat(inference): extract geospatial workloads from backend#673
frgfm wants to merge 5 commits into
mainfrom
codex/extract-geospatial-inference

Conversation

@frgfm

@frgfm frgfm commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Extract geospatial triangulation and timezone lookup from the database-facing backend into an independently deployed inference service.

  • Backend calls inference over authenticated internal HTTP; no Redis, shared cache, or local fallback.
  • Inference receives no PostgreSQL, S3, JWT, temporal-model, risk-service, or superadmin credentials.
  • Inference is attached only to an internal compute network; it has no database network route and no published port.
  • Validation-worker failures continue through the existing lease/retry/dead-letter path.
  • Delete, label, and unmatch operations precompute the resulting alert state before any mutation; inference failure returns HTTP 503 with database state unchanged.
  • Notification timezone lookup stays best-effort: lookup failure is logged and notification delivery is skipped.

Architecture

flowchart LR
    U["Clients / cameras"] -->|"published API :5050"| B["Backend\nDB + storage + JWT credentials\ndata and compute networks"]

    B -->|"data network"| DB[("PostgreSQL")]
    B -->|"data network"| S3["S3 / LocalStack"]
    B -->|"compute network\nBearer token"| I["Inference\ntriangulation + timezone\n1 worker by default"]

    I -. "no route" .-> X[("Database blocked")]
    I -. "no credentials" .-> Y["S3 / JWT / backend secrets"]

    classDef isolated fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef blocked fill:#ffebee,stroke:#c62828,stroke-dasharray:5 5;
    class I isolated;
    class X,Y blocked;
Loading

The backend remains the sole authority for persistence and orchestration. Inference is a stateless compute boundary with three endpoints:

  • POST /v1/triangulate
  • POST /v1/timezone
  • GET /status (health only; unauthenticated)

The two compute endpoints validate bearer authentication, coordinate bounds, finite numeric values, unique sequence IDs, timestamps, classifications, and canonical response shape.

Dependency and image impact

flowchart LR
    BEFORE["Backend before\n26 direct\n35 transitive-only\n61 total\n581.6 MB"]
    AFTER["Backend after\n19 direct\n29 transitive-only\n48 total\n299.3 MB"]
    INF["Inference after\n7 direct\n17 transitive-only\n24 total\n385.7 MB"]

    BEFORE -->|"remove 7 direct / 6 transitive-only\n-282.2 MB (-48.5%)"| AFTER
    BEFORE -->|"extract native geospatial closure"| INF
Loading
Image / state Direct Transitive-only Locked closure Docker image Delta
Backend before 26 35 61 581.6 MB
Backend after 19 29 48 299.3 MB -282.2 MB / -48.5%
Inference after 7 17 24 385.7 MB new independently scaled image

Dependency closures are counted per image from uv export --only-group <group> --no-dev --no-hashes --no-emit-project; “transitive-only” is the closure minus direct declarations. Packages shared by both groups are counted in both images. Raw image sizes therefore must not be summed as host disk usage because shared Docker layers may be deduplicated.

Image measurements use docker image inspect .Size on Linux/arm64 images built from the same checkout and python:3.11-slim Dockerfile:

  • baseline backend: 581,552,675 bytes
  • extracted backend: 299,322,028 bytes
  • inference: 385,675,379 bytes

Package decisions

Package Result
geopy deleted; geodesic projection uses pyproj.Geod
pandas deleted; typed records/lists/dicts replace DataFrame plumbing
numpy removed from backend and direct code; remains inference-only transitively
networkx inference-only for tested maximal-clique enumeration
pyproj inference-only for CRS/geodesic projection
shapely inference-only for polygon repair/intersection/centroid
timezonefinder inference-only; notification timezone lookup is remote

Failure semantics

sequenceDiagram
    participant W as Validation worker
    participant B as Backend
    participant I as Inference
    participant D as Database

    W->>B: validate sequence
    B->>I: authenticated triangulation
    alt inference succeeds
        I-->>B: canonical groups + location
        B->>D: persist alert state
    else timeout / 4xx / 5xx / malformed response
        I--xB: unavailable
        B-->>W: raise
        W->>D: retain due marker / retry lease
    end
Loading

Interactive mutations use the same ordering: fetch state → call inference → mutate only after success. There is no local compute fallback that could silently diverge.

Verification

  • 637 backend tests passed in the Docker Compose stack.
  • 12 inference tests passed, covering authentication, validation, deterministic output, timezone fallback, empty/singleton/relaxed-time/same-pose/same-mast/mixed/dateline behavior, and responsive health checks during serialized compute.
  • Ruff, ty, lock check, and dependency-sync verification passed.
  • Both production Dockerfiles build successfully.
  • Backend image cannot import geopy, networkx, numpy, pandas, pyproj, shapely, or timezonefinder.
  • Local isolation inspection: inference had only pyronear_compute, no sensitive environment variables, and could not resolve db.
  • Authenticated backend-to-inference smoke: triangulation 200; timezone 200 (Europe/Paris).

Synthetic in-process triangulation benchmark (median of 3 runs, Linux/arm64 dependency environment):

Sequences Median Output groups
5 2.5 ms 1
40 200.3 ms 780
80 2.62 s 3,160

Deployment and rollback

Release automation builds and publishes both images, uploads the current Compose definition, provisions INFERENCE_API_TOKEN from GitHub Secrets, starts healthy inference first, then switches backend. CPU work runs off the event loop behind a per-process lock, so triangulations remain serialized while /status and timezone lookup stay responsive. INFERENCE_WORKERS defaults to 1 and can scale independently later.

Rollback remains backend-only: restore the previous backend image; the stateless inference container may remain running.

Production acceptance still required

  • Configure a strong repository secret named INFERENCE_API_TOKEN for VPS rollout.
  • Run authenticated triangulation and timezone notification smoke tests.
  • Confirm inference has no database credentials or connectivity in production.
  • Confirm end-to-end alert creation on the VPS.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.42897% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.20%. Comparing base (729a870) to head (6d997b7).

Files with missing lines Patch % Lines
src/app/services/inference.py 87.01% 10 Missing ⚠️
src/inference/overlap.py 95.42% 7 Missing ⚠️
src/app/main.py 66.66% 2 Missing ⚠️
src/app/services/alerts.py 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #673      +/-   ##
==========================================
+ Coverage   93.74%   94.20%   +0.46%     
==========================================
  Files          59       62       +3     
  Lines        3147     3262     +115     
==========================================
+ Hits         2950     3073     +123     
+ Misses        197      189       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@frgfm

frgfm commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Adversarial review completed across OpenCode, Claude, Codex, and Cursor.\n\nApplied:\n- deploy workflow uploads the updated Compose file and provisions the shared inference token\n- CPU triangulation runs off the event loop and remains serialized per process\n- backend exposes a stable 503 response without leaking upstream details\n- zero-angle cone compatibility and concurrency/health regression coverage\n\nKept intentionally:\n- notification timezone lookup failures skip delivery, matching the agreed best-effort policy\n- no Redis, local fallback, or shared geospatial abstraction without evidence they are needed

@frgfm frgfm self-assigned this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant