Skip to content

fix(steam/download): close race that hangs Update/Verify at 0%#1434

Open
jeremybernstein wants to merge 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-update-verify-race
Open

fix(steam/download): close race that hangs Update/Verify at 0%#1434
jeremybernstein wants to merge 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-update-verify-race

Conversation

@jeremybernstein
Copy link
Copy Markdown
Contributor

@jeremybernstein jeremybernstein commented May 13, 2026

Description

Fixes a long-standing race in SteamService.downloadApp(...) that hangs Update and Verify at 0% on games whose depot work completes synchronously inside the launched coroutine.

downloadJobs[appId] = info was assigned after instance!!.scope.launch { ... } had already kicked off the download body. For a fast verify/update (game already on disk, DepotDownloader has no chunks to fetch) the launched coroutine can complete and reach its inline removeDownloadJob(appId) before the outer assignment runs. removeDownloadJob finds no entry, skips notifyDownloadStopped, and then the outer code populates the map with a now-stale completed-job DownloadInfo and emits notifyDownloadStarted. The UI sits at 0% with no stop event ever coming.

Cancel can't recover either: DownloadInfo.downloadJob may still be null (the setDownloadJob line hadn't run yet), and even if set, the job is already complete — so cancel() is a no-op. The retry path returns the same stale DownloadInfo via the if (downloadJobs.contains(appId)) return getAppDownloadInfo(appId) short-circuit at line ~1666, so the second attempt hangs identically.

Two surgical changes:

  • Register downloadJobs[appId] + emit notifyDownloadStarted before launching the coroutine. The inline removeDownloadJob inside the launch body now always finds the entry and emits DownloadStatusChanged(false).
  • invokeOnCompletion now unconditionally calls removeDownloadJob(appId) as a safety net for paths the inline call doesn't cover: the early return@launch on empty licenses, exceptions thrown before the catch handlers, and cancellations out of suspension points. The second call is a no-op when the inline path already removed the entry, so the existing event ordering is preserved on the success path (inline removeDownloadJobLibraryInstallStatusChanged → safety-net no-op).

Closes #1433

Recording

N/A — fix is for a hang. Before: progress bar parks at 0% forever on Update/Verify of an up-to-date title. After: completes and Play button returns. Happy to attach a screen recording if needed.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Verified on device

Built :app:assembleDebug, installed on a physical device, ran Update and Verify on an already-installed title repeatedly. No more hang at 0%; Play button returns reliably.


Summary by cubic

Fixes a race in SteamService.downloadApp that caused Update/Verify to hang at 0% on up-to-date games (fixes #1433). Ensures start/stop events emit correctly and cancel/retry work reliably.

  • Bug Fixes
    • Register downloadJobs[appId] and call notifyDownloadStarted before launching the coroutine so removeDownloadJob always finds the entry and the UI receives the stop event.
    • Call removeDownloadJob(appId) unconditionally in invokeOnCompletion to cover early returns, exceptions, and cancellations without leaving stale state.

Written for commit 9c0188b. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of download state tracking by ensuring notifications are sent immediately when downloads start, preventing stale state issues.
    • Enhanced download cleanup logic to consistently handle job completion and properly categorize cancellation events.

Review Change Stack

downloadJobs[appId] = info was assigned AFTER scope.launch{} kicked off
the work. on a fast verify/update (game already on disk, DepotDownloader
has no chunks to fetch) the launched coroutine could complete and call
removeDownloadJob(appId) BEFORE the outer assignment ran. removeDownloadJob
found no entry, skipped notifyDownloadStopped, then the outer code
populated the map with a stale completed-job DownloadInfo and emitted
notifyDownloadStarted. UI hung at 0% with no stop event ever coming.

cancel was a no-op too — DownloadInfo.downloadJob may still have been
null (setDownloadJob hadn't run), and even when set the job was already
complete. retry returned the same stale DownloadInfo from downloadJobs
via the line ~1666 short-circuit.

fix:
- register downloadJobs[appId] + notifyDownloadStarted BEFORE launching
  the coroutine, so the inline removeDownloadJob inside the launch body
  always finds the entry and emits DownloadStatusChanged(false).
- invokeOnCompletion now unconditionally calls removeDownloadJob as a
  safety net for paths the inline call doesn't cover (early return on
  empty licenses, exceptions before the catch handlers, cancellations
  out of suspension points). second call is a no-op.

event ordering for the success path is preserved: inline
removeDownloadJob fires first (DownloadStatusChanged false), then
LibraryInstallStatusChanged, then the invokeOnCompletion no-op.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4004c8db-64d6-4615-8cf4-6e63376aec31

📥 Commits

Reviewing files that changed from the base of the PR and between a634c4a and 9c0188b.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/service/SteamService.kt

📝 Walkthrough

Walkthrough

downloadApp(...) moves registration of downloadJobs[appId] and emission of the download started event to occur before the async download coroutine launches. The completion handler is refactored to always call removeDownloadJob(appId) in invokeOnCompletion, with cancellations logged separately, ensuring the UI progress state remains synchronized throughout the download lifecycle.

Changes

Download Job Lifecycle

Layer / File(s) Summary
Early download job registration
app/src/main/java/app/gamenative/service/SteamService.kt
downloadApp(...) stores the newly created DownloadInfo in downloadJobs[appId] and emits the "download started" event before the async download coroutine is launched.
Unconditional completion cleanup
app/src/main/java/app/gamenative/service/SteamService.kt
The completion handler in invokeOnCompletion always calls removeDownloadJob(appId) and logs cancellations separately based on whether the throwable is a CancellationException.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • utkarshdalal/GameNative#806: Modifies UI logic that determines download progress/pause-resume via downloadInfo lookups, directly affected by this PR's changes to downloadJobs[appId] registration timing.
  • utkarshdalal/GameNative#321: Overlaps in the same download completion/job-handling logic area, modifying completion and failure cleanup flow in SteamService.
  • utkarshdalal/GameNative#388: Also addresses download job completion and cleanup in SteamService, removing download job and moving completion logic.

Poem

🐰 A race condition caught in the wild,
Where progress bars hung and downloads filed.
Now jobs register first, and cleanup's sure—
The UI stays synced, the lifecycle pure! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: closing a race condition that causes Update/Verify to hang at 0%. It is specific, concise, and directly relates to the core change.
Description check ✅ Passed The description is comprehensive and well-structured, covering the root cause, reproduction path, and detailed technical solution. All key template sections are completed with thorough context and verification details.
Linked Issues check ✅ Passed The PR directly addresses issue #1433 by fixing the race condition that caused Update/Verify to hang at 0% on already-installed games. The two surgical changes (early registration and safety-net handler) ensure the UI receives proper completion events and retry/cancel work reliably.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to fixing the race condition in SteamService.downloadApp(...). No unrelated modifications were introduced; the alterations target only the specific issue of event ordering and state management in the download lifecycle.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

@utkarshdalal
Copy link
Copy Markdown
Owner

Please upload a recording - I have not seen this issue before

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

Please upload a recording - I have not seen this issue before

I'm not sure why I would make this up, but here. I left it at a minute or so, but it's still hung at 0% while I upload this:

screen-20260517-171532.mp4

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.

[Bug]: Update and Verify hang at 0% on already-installed games

2 participants