Skip to content

Fix encounter_list memory by using DB-level pagination#228

Open
alexiadowns-canvas wants to merge 1 commit intomainfrom
fix/encounter-list-memory-optimization
Open

Fix encounter_list memory by using DB-level pagination#228
alexiadowns-canvas wants to merge 1 commit intomainfrom
fix/encounter-list-memory-optimization

Conversation

@alexiadowns-canvas
Copy link
Copy Markdown

@alexiadowns-canvas alexiadowns-canvas commented Apr 3, 2026

Summary

  • Both pagination paths (_sort_and_paginate_database and _sort_and_paginate_delegated_orders) were calling list(note_queryset), loading the entire result set into memory before slicing for pagination. For large instances like Doctronic, this caused 100–500 MB memory spikes per request, leading to OOM-related 502 outages.
  • Normal sort path: replaced list(queryset) + Python slicing with Django queryset slicing (queryset[offset:offset+page_size]), which translates to SQL OFFSET/LIMIT.
  • Delegated orders path: replaced the full-queryset materialization + per-note Python loop (N+1 queries) with DB-level Subquery annotations on ImagingOrder.delegated and Referral.forwarded, enabling database-side sort, filter, and pagination.
  • The exact delegated orders count (which checks open tasks via the JSON task_ids field) is still computed per-note in the render loop — but only for the ~25 notes on the displayed page, not all notes in the system.
  • Removed the now-unused _apply_pagination helper.

Test plan

  • Deploy to a staging instance and verify encounter list loads correctly with default sort (date of service)
  • Verify sorting by each column still works (patient name, provider, location, note title, DOS, billable, uncommitted commands, delegated orders, claim queue)
  • Verify the "has delegated orders" filter returns correct results
  • Verify pagination (next/previous, page numbers) works correctly
  • Monitor plugin runner memory logs after deploy — spikes should drop from 100-500MB to single-digit MB

…of loading all notes

The encounter_list plugin was loading entire querysets into Python memory
before paginating, causing 100-500MB+ memory spikes per request. This
caused production outages on Doctronic (Pylon #22626) and affects all
65 instances running this plugin.

Changes:
- _sort_and_paginate_database: use Django queryset slicing (OFFSET/LIMIT)
  instead of list(queryset) followed by Python slicing
- _sort_and_paginate_delegated_orders: replace full-queryset materialization
  + per-note Python loop with DB-level Subquery annotations on ImagingOrder
  and Referral, enabling DB-side sort/filter/pagination
- Remove unused _apply_pagination helper

The delegated orders DB annotation is approximate (does not check the JSON
task_ids field for open tasks), but the exact count is still computed for
the ~25 notes on each rendered page via _calculate_delegated_orders_count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alexiadowns-canvas alexiadowns-canvas changed the title Fix encounter_list memory explosion: use DB-level pagination Fix encounter_list memory by using DB-level pagination Apr 3, 2026
reverse=(sort_direction == "desc")
"""Sort and paginate for delegated orders using database-level annotation.

Uses ImagingOrder and Referral FKs to Note to compute an approximate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gather that this change will result in a higher count on some views than on others, because it will be the superset of all tasks, not just those that are open. Is that change in behavior going to be acceptable to users?

.annotate(cnt=Count("id"))
.values("cnt")
)
note_queryset = note_queryset.annotate(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing indexes on forwarded and delegated, which might force a sequential scan. We need to look at an explain plan for this new query with the annotation and subqueries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude What SQL will be generated by lines 259-264?

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.

2 participants