Skip to content

Instant: stop stale background sync when embedded views are disposed (workaround + stress example) - #12

Draft
mutumbakato wants to merge 1 commit into
masterfrom
instant-stale-sync-workaround
Draft

Instant: stop stale background sync when embedded views are disposed (workaround + stress example)#12
mutumbakato wants to merge 1 commit into
masterfrom
instant-stale-sync-workaround

Conversation

@mutumbakato

@mutumbakato mutumbakato commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Closing a NutrientInstantView on Android does not stop that document's listen-for-server-changes long-poll. The InstantPdfDocument survives in the descriptor cache with its sync coordinator still listening, so every document ever viewed keeps a background /sync connection open. After viewing enough distinct documents, these stale long-polls exhaust the per-host connection pool and the next document's auth/download queues behind them:

  • Document loads stall for 40–60 seconds (measured 47.6 s stalls on a healthy local Document Engine, no fault injection).
  • Behind a reverse proxy with a read timeout, the stale polls die as endless empty-body 502s for previously viewed documents.
  • iOS is unaffected: it correctly stops sync on view teardown (verified side by side).

What this PR contains

1. InstantSyncLifecycleAdapter (in the new example) — a workaround using only public APIs and the stock pub.dev packages:

  • At onPdfFragmentReady, disables listenToServerChangesWhenVisible before the document loads. Timing matters: an already-running long-poll holds its pool slot until the server releases it (~47–60 s measured), so stopping at dispose alone is not enough (we measured that variant: stalls unchanged).
  • At onFragmentDetached, stops listening on the cached document (belt and braces).
  • syncNow() pulls server changes on demand, replacing the disabled real-time listener. Local edits still upload automatically, and every push also applies server changes.

Registered once via platform.Nutrient.initialize(androidAdapter: ...). Verified against a licensed Document Engine that adapter registration does not affect licensing.

2. "Instant Switch Stress" example — automates document-switch / close-reopen cycles against a Document Engine and reports per-cycle load latency, with a UI toggle to compare baseline vs workaround.

3. Fixes example/pubspec.yaml — removes dependency_overrides pointing outside the repository (monorepo sync artifact that broke pub get on clean checkouts).

Measured results (12 distinct fresh documents per run)

Configuration Median load Stalls
Baseline 746 ms 47.6 s at cycles 6 and 11
Stop-at-dispose only 767 ms unchanged (in-flight poll keeps its slot)
This adapter ~250 ms none

Server-side confirmation: without the workaround, Document Engine logs show all previously viewed documents still re-polling /sync minutes after their views were disposed; with it, only the mounted document syncs.

Trade-off

While a document is passively viewed it no longer receives other users' changes in real time; changes arrive via syncNow(), on any local edit, or the periodic background sync. For single-user review workflows this is typically acceptable. The proper SDK fix (full sync cleanup on view teardown, matching iOS) is tracked internally and lands in an upcoming release.

Try it

cd example && flutter run
# open "Instant Switch Stress (repro #134164)", toggle the workaround, Run

The example defaults to a local Document Engine on :5001 with documents fresh-doc-01..30 and clean-doc-a uploaded (see the header of instant_switch_stress_example.dart for setup). To adopt the workaround in an app, copy InstantSyncLifecycleAdapter from the example and register it at startup — license initialization via Nutrient.initialize(...) is unaffected.

Closing a NutrientInstantView on Android does not stop the document's
listen-for-server-changes long-poll: the InstantPdfDocument survives in
the descriptor cache with its sync coordinator still listening, so every
document ever viewed keeps a background /sync connection open. After
viewing enough distinct documents these stale long-polls exhaust the
per-host connection pool and the next document's auth/download queues
behind them, surfacing as 40-60 s document loads (and, behind a reverse
proxy with a read timeout, as endless empty-body 502s for previously
viewed documents). iOS already stops sync on view teardown.

The new "Instant Switch Stress" example demonstrates both the issue and
the workaround, using only public APIs and the stock pub.dev packages:

- InstantSyncLifecycleAdapter (an AndroidAdapter) prevents the
  listen-for-changes long-poll from starting at onPdfFragmentReady and
  stops listening on the cached document at onFragmentDetached. Timing
  matters: an already-running long-poll holds its pool slot until the
  server releases it (~47-60 s measured), so stopping at dispose alone
  is not enough.
- syncNow() pulls server changes on demand, replacing the disabled
  real-time listener (local edits still push automatically).
- The example automates document-switch / close-reopen cycles against a
  Document Engine and reports per-cycle load latency, with a UI toggle
  to compare baseline vs workaround.

Measured on a 12-distinct-document sweep: baseline median 746 ms with
47.6 s stalls; with the adapter enabled median ~250 ms and no stalls.
Verified against a licensed Document Engine that adapter registration
does not affect licensing.

Also removes the example's dependency_overrides that pointed outside
the repository (a monorepo sync artifact that broke pub get on clean
checkouts); all platform packages now resolve from pub.dev.

Repro/verification for https://pspdfkit.zendesk.com/agent/tickets/134164

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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