Skip to content

fix: serve patient panel photos from the database instead of per-row FHIR calls - #411

Open
canvas-investigator[bot] wants to merge 1 commit into
mainfrom
patient-panel-db-photos
Open

fix: serve patient panel photos from the database instead of per-row FHIR calls#411
canvas-investigator[bot] wants to merge 1 commit into
mainfrom
patient-panel-db-photos

Conversation

@canvas-investigator

@canvas-investigator canvas-investigator Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

KOALA-6442

🧵 Jessica Herbert cc @JessicaHerbert

Problem

The patient panel rendered one <img> per patient row pointing at the app's own GET /app/<patient_id>/photo endpoint. Each request made synchronous HTTPS calls back into the same instance — first POST /auth/token/ (SDK Http, 30s timeout), then a fumage GET /Patient/<id>. On a large page that's ~40–50 concurrent self-referential requests competing for the very web-worker pool that must serve them. The pool gets exhausted, a worker crashes (exited with code 1), and HAProxy returns 502 for the whole instance (photos, /auth/token/, /healthcheck, /patient/<id>, /graphql).

The previous mitigation (in-memory token cache + a 4s photo-fetch timeout) was insufficient: the token cache is a per-process class dict that cold-starts empty after every plugin-runner restart, so a concurrent burst still stampedes /auth/token/ — and the short timeout was applied only to the fumage GET, never to the token POST, which kept the 30s timeout and drove a ReadTimeout deadlock.

Fix

1. Serve photos from the database — no self-HTTP. Patient photos are now read via patient.photo_url (the SDK's presigned URL over the photos relation, table canvas_sdk_data_api_patientphoto_001) — exactly how this plugin already reads staff photos. photos is prefetched on the table queryset (ordered so .first() stays on the prefetch cache — no N+1) and the URL is resolved in services/serialization.py. This deletes the /<patient_id>/photo endpoint, _get_fhir_token, _fetch_patient_photo_data, _ShortTimeoutHttp, and services/fhir_photo.py — removing 100% of the plugin's self-referential HTTP. Photos now load from S3/CloudFront, never the instance web tier.

2. Debounce the stats recompute. Every note/task/address/patient/protocol write triggered a full 6-query single-patient recompute, and one save cascades several events — each rerunning the whole thing. handlers/panel_stats_sync.py now coalesces per-patient recomputes within a short window (cache failures degrade to "recompute anyway", never "skip"). The 15-minute reconcile cron remains the correctness backstop.

Scope notes

  • The token single-flight / short-timeout stopgap floated during triage is obsoleted by fix add ref paper #1 (the token path is gone entirely), so it is intentionally not included.
  • The table's <img> already carries loading="lazy" (static/table.html), so lazy-loading needed no change; PAGE_SIZE remains a per-instance secret for operators who want a smaller default page.
  • Removed the now-unused FHIR_CLIENT_ID / FHIR_CLIENT_SECRET / CANVAS_INSTANCE_URL secrets from the manifest and README; bumped plugin_version to 2.1.0.

Testing

Added/updated pytest coverage — _patient_photo_url (property read + error fallback), the serialization default-avatar assertion, and a stats-handler debounce test; removed the obsolete FHIR-token and photo-endpoint tests. I could not execute the DB-backed suite in this environment (it needs the canvas-plugins pytest-django + Postgres harness); the tests follow the repo's existing factory / django_db patterns and should run in CI. All changed files pass py_compile and the manifest is valid JSON.

Generated by the Investigator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant