Skip to content

Fix torrent loss on restart, session-shutdown ANR, and player icon overlap#17

Merged
jpcottin merged 1 commit into
masterfrom
fix/resume-persistence-anr-review
Jun 11, 2026
Merged

Fix torrent loss on restart, session-shutdown ANR, and player icon overlap#17
jpcottin merged 1 commit into
masterfrom
fix/resume-persistence-anr-review

Conversation

@jpcottin

Copy link
Copy Markdown
Owner

Problem

  1. Torrents disappeared after app restart even though their data was still on disk.
  2. ANR when interacting with the app right after the session was recreated (e.g. after granting All-Files-Access).
  3. Player CC button rendered as an unreadable blob — the subtitle and audio-track buttons were stacked on top of each other.

Root causes & fixes

Resume-data persistence

  • onStop saved resume data on lifecycleScope (cancelled at ON_DESTROY) while onDestroy released the session on the main thread; if release won the race, nothing was saved. A new SessionLifecycleCoordinator serializes init/save/release on a FIFO queue owned by a process-wide scope — release can never run before a pending save.
  • Resume files were written non-atomically; a process kill mid-write truncated previously-good files. Writes now use tmp + rename; corrupt files are quarantined as .corrupt and recovered via magnet re-add (the filename embeds the info-hash, so libtorrent rechecks the data already on disk).
  • Resume data was only saved at onStop; it is now also requested eagerly on add_torrent, metadata_received, and torrent_finished alerts.

ANR

  • nativeRelease held the global native mutex while libtorrent waited on tracker stop-announces (~27 s observed), blocking a main-thread addMagnet call. It now abort()s under the lock and finishes shutdown on a detached thread via session_proxy (re-init latency went from ~27 s to ~1 ms).
  • All native calls moved off the main thread: DefaultDataRepository now owns the threading contract, routing every mutating call through the session queue — which also waits for pending init, so calls can no longer hit an uninitialized session.

UI

  • Player: subtitle/audio buttons BoxRow (they were overlapping).
  • Copy magnet: no longer silently writes an empty string to the clipboard; shows a toast on failure.
  • Removed dead background-mode plumbing (onAppBackground/onAppForeground, setBackgroundMode).

Tests

  • 9 new JVM unit tests for the coordinator, including a regression test for the save/release race (onDestroy_neverReleasesSessionBeforePendingSave).
  • 5 new instrumented persistence tests: restart round-trip, eager resume write, corrupt-file quarantine, magnet recovery, removed-torrent non-resurrection.
  • Full suite green: 29 JVM + 38 instrumented on an Android 15 emulator. CC fix verified visually on-device.

…erlap

Torrents could disappear from the list after an app restart even though
their data was still on disk. Three causes, all fixed:

- onStop saved resume data on lifecycleScope (cancelled at ON_DESTROY)
  while onDestroy released the session on the main thread; when release
  won the race, nothing was saved. A new SessionLifecycleCoordinator now
  serializes init/save/release on a FIFO queue owned by a process-wide
  scope, so release can never run before a pending save.
- Resume files were written non-atomically; a process kill mid-write
  truncated previously-good files. Writes now go through tmp + rename,
  and corrupt files are quarantined as .corrupt and recovered via a
  magnet re-add (the filename embeds the info-hash).
- Resume data was only saved at onStop. It is now also requested eagerly
  on add_torrent, metadata_received, and torrent_finished alerts.

The session-recreate path (save path change) caused an ANR: nativeRelease
held the global mutex while libtorrent waited on tracker stop-announces
(~27s), blocking a main-thread addMagnet call. nativeRelease now aborts
the session under the lock and finishes shutdown on a detached thread via
session_proxy, and all native calls moved off the main thread: the
repository owns the threading contract, routing every mutating call
through the session queue (which also waits for pending init, so calls
can no longer hit an uninitialized session).

Also fixed: subtitle (CC) and audio-track buttons in the player rendered
stacked on top of each other (Box -> Row), Copy magnet silently writing
an empty string to the clipboard, and dead background-mode plumbing
(onAppBackground/onAppForeground, setBackgroundMode) removed.

Tests: 9 JVM unit tests for the coordinator (including a regression test
for the save/release race) and 5 instrumented persistence tests
(restart round-trip, eager save, quarantine, magnet recovery, removal).
@jpcottin jpcottin merged commit a40b563 into master Jun 11, 2026
5 checks passed
@jpcottin jpcottin deleted the fix/resume-persistence-anr-review branch June 11, 2026 05:50
jpcottin added a commit that referenced this pull request Jun 11, 2026
…epository

The DataRepository interface changes in #17 (suspend mutating methods,
setBackgroundMode removed) missed MainScreenTest's inline fake, breaking
compileDebugAndroidTestKotlin in CI. The local run before pushing had
reused a stale test APK, which masked the error.
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