Repository navigation
fix: cap explicit page sizes and stop printing query results - #8
Conversation
An explicit /api/v1/data limit had no maximum, so one request could load all 35,753 records, and get_filtered_data printed every record with rich.print, which peaks at about 1.6 GiB for the full table. On 2026-11-04 this service was OOM-killed while its node went NotReady. Cap an explicit limit at AUTO_PAGINATION_THRESHOLD the same way a missing one already is, with next/previous links, and drop the print.
matthewberry
left a comment
There was a problem hiding this comment.
Reproduced the PR's claims against a throwaway Postgres: the full suite gives 21 passed, 1 skipped, and with app/ reverted to origin/main both new tests fail.
Two problems worth fixing before merge. Both are in code this PR doesn't touch, but the PR now sends every client asking for limit > 5000 through it.
1. Following next stops after page 2 (reproduced). The next link carries limit=5000. On that request, params.limit > settings.AUTO_PAGINATION_THRESHOLD (app/routers/data.py:198) is false, so the response has no auto_paginated and no next. With the threshold monkeypatched to 10:
PAGE1 next: http://testserver/api/v1/data?format=json&offset=10&limit=10
PAGE2 keys: ['limit', 'offset', 'total'] next: None
This was already broken for clients that omit limit. A client that follows next gets two pages and then a response that looks like the end of the data, unless it compares offset + limit to total. The PR body's "auto_paginated with next/previous links" only holds for the first page.
- Suggested fix:
params.limit >= settings.AUTO_PAGINATION_THRESHOLD, so pages after the first keep their links. - Test: one that follows
nexttwice. The skippedtest_pagination_navigationwas meant to cover this.
2. CSV results are cut off with no signal (reproduced). ?format=csv&limit=1000 with the threshold at 10 returned 10 rows. The response headers were only content-disposition, content-type, vary and content-encoding, because the CSV branch never adds pagination metadata. Before this PR, an explicit large limit was how a CSV client got everything from the API. Now it gets 5,000 rows and can't tell anything is missing. The full-dataset .gz downloads in the API description reduce the impact.
- Suggested fix: add a
Link: <…>; rel="next"orX-Total-Countheader to the CSV response, or at least document that CSV output is capped.
Didn't investigate: who calls /data with limit above 5000. If the OED frontend or a known script relies on uncapped limits, this change breaks it. Worth checking before merging.
…es them too The next link carries limit=threshold, and a limit equal to the threshold was not treated as paginated, so following next stopped after page 2. Use >=. A CSV response has no body metadata, so a capped one gave no sign anything was missing. Send the next/prev links in a Link header and the total in X-Total-Count. The link-building moves above the format branch so both formats share it. Implements the skipped test_pagination_navigation.
|
Both fixed in ede0f1c. 1. 2. Capped CSV. CSV responses now carry Who calls Full suite: 23 passed. |
Why
On 2026-11-04 this service was the container OOM-killed at 15:31 UTC while its node (
mmli2-worker-cpu-03) went NotReady, evicting every pod on it. Two things in the/api/v1/datapath make one request expensive:limithad no maximum. Auto-pagination only applied whenlimitwas omitted (app/routers/data.py), so?limit=100000loaded all 35,753 records.get_filtered_dataranrich.print(f"{records=}")(app/db/queries.py:143). Measured locally with synthetic records shaped like the real ones, the print peaks at about 260 MiB for 5,000 records and about 1.6 GiB for the full table. It also wrote 33k log lines per page.#7 adds a 1 GiB memory limit so this container, not the node, absorbs the next mistake. This PR removes the cause.
What
limitofAUTO_PAGINATION_THRESHOLD(5000) or more is now treated like a missing one: the query is limited to 5000, and the response setsauto_paginatedwithnext/previouslinks. Alimitbelow 5000 behaves as before.nextlink carrieslimit=5000, which previously wasn't treated as paginated, so followingnextstopped after page 2 (true before this PR for clients that omittedlimit).Linkheader (rel="next",rel="prev") and the total inX-Total-Count. The link-building moved above the format branch so both formats share it.rich.printof query results is gone. (richstays inpyproject.toml; the SQL tests use it.)/datadocstring describe the cap and the CSV headers.This changes behavior for clients that send
limitabove 5000: they get 5000 records and anextlink instead of everything.Testing
New tests in
tests/test_auto_pagination.py, each failing without the fix:?limit=1000with the threshold at 10 sendsLIMIT 10to the database and returnsauto_paginatedwith anextlink.test_pagination_navigation(previously skipped) followsnextfrom the first page to the last: offsets 0, 10, 20 with 25 records.?format=csv&limit=1000returnsX-Total-Count: 100and aLinkheader with the next page, keepingformat=csv.Full suite: 23 passed. Before: 19 passed, 1 skipped.
Not handled: the app's
CORSMiddlewaresets noexpose_headers, so browser JavaScript on another origin can't readLinkorX-Total-Count. Scripts and plain downloads aren't affected.Note:
app.main's lifespan connects to a real database even though these tests mock every query, so they need a reachable Postgres. I ran them against a throwaway localpostgres:16-alpinewithPYTHONPATH=..Without the print, a 5,000-record page peaks at about 21 MiB and the full table at about 71 MiB in plain Python (synthetic records, before FastAPI's encoding), well under #7's 1 GiB limit.
Deploying
Merging builds
moleculemaker/mmli-oed-api:latest. The Deployment pulls:latestwithimagePullPolicy: Always, but nothing in its spec changes, so the new code is live only after the pod restarts (kubectl -n openenzymedb rollout restart deploy/mmli-oed-api, after the image build finishes).