Skip to content

fix(seerr): stop the requests list from reloading forever on large instances - #185

Merged
retransmit merged 11 commits into
retransmit:developmentfrom
v3DJG6GL:fix/seerr-requests
Oct 12, 2026
Merged

retransmit merged 11 commits into
retransmit:developmentfrom
v3DJG6GL:fix/seerr-requests

Conversation

@v3DJG6GL

@v3DJG6GL v3DJG6GL commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Issue

The request provider started a 10-second refresh timer when a fetch began, rather than when it ended. /request returns at most 100 rows per call, so the full list is read page by page. With ~1500 requests that's 15 calls in a row. If those take longer than 10s, the timer fires mid-read and everything starts over from page 0. On big instances the list never finishes loading.

Changes

  • The provider uses ref.polled, so the next refresh is only scheduled after the current read is done.
  • Unfiltered reads get the total from /request/count first, then fetch all pages in parallel (page order is kept).
  • The old page-by-page loop is still used when a filter is set, or when the count has no usable total (401/403, 5xx, odd body, 0).
  • If the count call times out or can't reach the server, the read fails right away instead of trying every page into the same error.
  • seerrMediaDetailsProvider is now autoDispose with a 5 min keep-alive. Before, it kept one cached response per title ever scrolled past, for as long as the app ran.

Size

added removed
code 35 10
comments 25 3
tests 226 0

Testing

  • flutter analyze is clean and all 21 tests in service_seerr pass.
  • New tests: pages are requested together and returned in order. If one page fails, the entire read fails. If the count is 0 or 1 with no total, it falls back to the loop. The 2000-item cap is still in place. Timeout and unreachable errors fail without paging. A slow read isn't restarted by the poll..

LLM Disclosure

Developed with assistance from Claude Code (Opus 5.5). The LLM helped me identify the related files, code parts and proposed possible code modifications and fixes.
I've reviewed, cleaned, refactored and hardened the code the best I could, but my Dart capabilities are not that good.

@Bhavyashah94 Bhavyashah94 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Testing & Verification Summary

Tested branch pr-185 across both automated test suites and end-to-end on an Android emulator (Pixel 7, API 35 / Android 15) against a live Seerr backend instance.

1. Automated Tests & Static Analysis

  • Unit & Provider Tests: Ran flutter test in services/service_seerr — 21/21 passed.
    • Verified parallel page retrieval maintains proper ordering across multi-page responses.
    • Verified graceful fallback to sequential pagination if /request/count fails.
    • Verified the 50-page safety cap enforcement.
    • Verified polling timer interval only begins counting after the entire fetch finishes.
    • Verified seerrMediaDetailsProvider cache retention and 5-minute keep-alive auto-dispose.
  • Static Analysis: dart analyze services/service_seerr reported 0 errors, 0 warnings, 0 lints.

2. Android Emulator Verification

  • Installed debug build of the app on an Android emulator and connected to a live Seerr instance.
  • Requests View: Navigated to the Seerr Requests screen; requests loaded cleanly and quickly without enters into infinite reload loops or UI thrashing.
  • Scrolling & Posters: Scrolled through the requests list; poster images loaded reactively through the media details provider without hiccups or re-triggering parent fetches.
  • Pull-to-Refresh: Performed pull-to-refresh; settled cleanly and waited the configured polling interval before any background refresh.
  • Logcat Monitoring: Observed zero unhandled exceptions, zero UI thread starvation, and clean Impeller frame rendering during navigation and refresh cycles.

3. Scope & Coverage

  • Covered: Parallel page fetching, sequential fallback, safety cap, chained polling interval (ref.polled), poster caching auto-dispose, live backend requests retrieval, Android emulator UI rendering and pull-to-refresh.
  • Not Covered: Very large instances (>1,000 requests requiring 10+ pages concurrently) on live hardware (verified via mocked unit tests), and live request mutation actions (approve/decline/delete) as this PR strictly touches fetching and polling logic.

Everything tested functions as intended and cleanly resolves the reload loop issue. Leaving as a review comment for other testers to verify before merge.

@lxBlazarxl

Copy link
Copy Markdown
Collaborator

Thanks for the fix — moving the refresh logic from an uncoordinated Timer onto ref.polled is definitely the right shape and resolves the endless reload loop. Making seerrMediaDetailsProvider autoDispose with keep-alive also fixes a real memory accumulation issue.

Building on the verification above, a few edge-case and failure-mode observations to consider:

  1. Unbounded fan-out on large instances (seerr_api.dart:88)
    With pageSize = 100 and maxItems = 2000, getAllRequests can fire up to 20 concurrent HTTP requests against the Seerr backend at once. On low-powered home servers (e.g. Raspberry Pi/NAS) or flaky mobile connections, this burst can cause connection throttling or socket exhaustion. Bounding concurrency (e.g. 4–6 pages at a time via a pool or chunked batches) would preserve the latency win without the aggressive burst.

  2. Single page failure triggers 5× backoff (seerr_api.dart:88)
    Under Future.wait, if a single page among 15–20 fails (e.g. transient 5xx or network blip), the entire read throws. ref.polled then applies the default 5× error backoff (10s * 5 = 50s). With fan-out increasing the statistical probability of a single failed page, retrying a failed page once before failing the whole batch would prevent dropping the entire list for 50s.

  3. NetworkAuthException on /request/count (seerr_api.dart:124)
    If /request/count returns a 401/403 due to invalid credentials, returning null causes a fallback to getRequests(skip: 0), which immediately 401s again. Rethrowing NetworkAuthException directly avoids that redundant second network hop.

  4. Coverage gaps & Testing notes

    • seerrMediaDetailsProvider: The autoDispose + 5-minute keepAlive addition currently has no unit test in service_seerr. Since this was a silent leak, an automated lifecycle test would prevent silent regressions.
    • The safety cap in code is 20 pages (2000 max items / 100 page size), not 50.
    • When hitting the 2000-item limit, a logDebug would make truncation visible in logs for troubleshooting.

Overall great improvements — happy to approve once the concurrency fan-out and error retry behavior are tuned!

…detail cache


The requests provider armed a 10s invalidation timer at fetch start, so a
multi-page drain slower than 10s restarted from page 0 forever - endless
loading on large instances (~1500 requests = 15 serial round-trips). The
refresh now goes through ref.polled (armed after the fetch settles), the
unfiltered drain reads /request/count and fetches all pages concurrently,
and per-title detail lookups are autoDispose with a 5-minute keep-alive
instead of cached forever.
…cannot be read


A failed /request/count fell back to the 2000-item safety cap, so every
refresh fired 20 concurrent page requests regardless of instance size.
Without a total the page count is unknown, so take the serial short-page
loop instead. Add tests for the concurrent paging, the cap, and the
fallback.
SeerrCounts.total defaults to 0, so a /request/count reply without a
total (an older or variant server, a proxy answering {}) planned zero
pages and the Requests list came back empty with no error. Treat 0 like
an unknown count: the serial loop costs one request on a truly empty
server and still reads every page on one that sent no total.
…the server


Every failure of /request/count fell back to serial paging, so a server
that does not answer burned the full timeout on the count and then again
on page 0 - twice as long before the error shows as before the count was
read. Rethrow timeouts, unreachable, TLS and cancelled errors; keep the
fallback for a server that answered (404 on older builds, 5xx, an odd
body, or a refusal, which may be a permission the list does not need).
… kept in page order


The fake answered every page at once and in turn, so the order check
passed even for pages gathered as they arrive, and a serial loop asks
the same three pages for 250 requests - nothing showed the pages were
fetched together. Hold each page until the test lets it go, check all
are asked before any answers, and let them answer last first. Add a page
that fails mid-way (an error, never part of the list), and say what the
fake actually mimics instead of "the way the server does".
…arted


The branch's headline fix - refresh after the read settles instead of on
a blind 10s timer - had no test. A read that takes 15s now finishes,
and the next starts 10s after it; the old timer started a second read at
10s (checked: this test fails on it, 2 reads instead of 1).
…erver throws


The getAllRequests doc still promised the serial loop whenever the count
could not be read; since timeouts and unreachable servers rethrow, only
a count the server answered without a usable total falls back.
… count at once


Only a timeout drove the transport rethrow in _requestTotal; a server
that is down or refuses the connection - the commoner case - could drop
out of the list and the suite stayed green while the count and page 0
each failed in turn. Checked: removing it from the list fails this test.
…d at once


The getAllRequests doc said serial paging was dropped because refresh
cycles could lap it, but the requests provider now arms its next cycle
only after a read settles, so nothing can lap it either way. What the
concurrent pages still buy is a shorter wait on every load and refresh.
_requestTotal's doc named only an unreachable server as the reason to
throw, though a bad certificate and a cancel throw too, and left the
refusal fallback out of its list. The test fake claimed to send only
the fields the client reads but also sent pageInfo, which nothing
parses, and called its requests "newest first" while numbering them
upwards; drop the pageInfo and say the ids are list places.
The doc said an untrusted certificate throws, but dio reports a failed
handshake as an unknown error, which falls back to paging and fails on
the first page instead; "a refusal" also read like a refused connection,
which does throw. Name the timeout and no-connection cases that the
tests pin, and the 401/403 that falls back.
@retransmit

Copy link
Copy Markdown
Owner

Tested against a live Seerr instance. The concurrent paging and settled-refresh polling work cleanly, and the request list loads fast and reliably. LGTM!

@retransmit
retransmit merged commit be6ace0 into retransmit:development Oct 12, 2026
1 check passed
@v3DJG6GL
v3DJG6GL deleted the fix/seerr-requests branch October 12, 2026 17:18
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.

4 participants