Skip to content

fix(offline): stop the launch cleanup exhausting file descriptors and clearing downloaded books - #1336

Merged
everpcpc merged 2 commits into
mainfrom
fix/orphan-cleanup-fd-exhaustion
Nov 11, 2026
Merged

everpcpc merged 2 commits into
mainfrom
fix/orphan-cleanup-fd-exhaustion

Conversation

@everpcpc

Copy link
Copy Markdown
Member

Problem

With a large offline library, launching the app could make downloads fail with SQLite error 14: unable to open database file and leave downloaded books cleared.

The launch sweep (cleanupOrphanedFiles) checks every book directory for files. That check used a directory enumerator and returned as soon as it found a file, which leaves the enumerator holding an open descriptor until an autorelease pool drains. The sweep walks all book directories as one job, so past roughly 250 downloaded books the process ran out of file descriptors.

From that point every directory read failed, and the check reported a failed read as "no files". The sweep then treated the remaining downloaded books as having lost their content: it tried to delete their directories and reset them to not downloaded. Because the limit is process-wide, downloads, database writes, and log writes failed during the same window.

The reporter's exported log shows this after each launch: thousands of Failed to cleanup orphaned directory (POSIX 24, too many open files) and Failed to persist download status (SQLite 14) entries.

Change

  • directoryContainsFiles reads each directory level in one call instead of keeping an enumerator open, so the sweep holds no descriptors between books.
  • It throws when a directory cannot be read. A missing directory still counts as holding no files.
  • The sweep skips directories it cannot inspect, neither deleting them nor resetting their status, and logs one summary line with the count and the first error.
  • removeBookDirectoryIfEmpty leaves the directory in place when it cannot be read.
  • The repo-conventions skill records the invariant: an unreadable directory is not an empty one.

Books that an earlier build already reset to not downloaded are not restored; they need to be downloaded again.

Validation

  • make build-ios, make build-macos, make build-tvos pass.
  • A standalone harness that runs the same loop inside an actor over 602 book directories with a 256-descriptor limit: the old check peaks at 256 descriptors and reports 347 directories as empty; the new one peaks at 4 and reports none wrongly, an empty directory reads as empty, and an unreadable one throws.
  • Not exercised in the app on a device or simulator.

Includes build 687.

Related to #1330.

🤖 Generated with Claude Code

…eleting unreadable books

The launch sweep checked every book directory with an early-exited directory enumerator. Each one kept its descriptor open until an autorelease pool drained, and the sweep runs as a single job, so libraries past roughly 250 downloaded books ran the process out of file descriptors. From then on every directory read failed, read as empty, and downloaded books were treated as having lost their content: the sweep tried to delete them and reset them to not downloaded, while downloads, database writes, and logging failed alongside.

The directory check now reads each level in one call and throws when a directory cannot be read. The sweep skips directories it cannot inspect and reports them once.
@everpcpc
everpcpc marked this pull request as ready for review November 11, 2026 11:11
@everpcpc
everpcpc merged commit 6223c9b into main Nov 11, 2026
3 checks passed
@everpcpc
everpcpc deleted the fix/orphan-cleanup-fd-exhaustion branch November 11, 2026 11:12
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