Guard ArrivalsViewModel.refresh() against out-of-order overlapping refreshes (#1933)#1934
Conversation
…freshes (#1933) refresh() applied every completed result unconditionally. The 60s poll loop awaits its suspend refresh() sequentially so it never overlaps itself, but manualRefresh() and loadMore() each viewModelScope.launch { refresh() } — so a user refresh (toolbar button, or the alert-dialog dismiss in ArrivalActionHandlers) can run concurrently with an in-flight poll and finish out of order. The older-started completion's `loaded.value = data` would then clobber the fresher result, surfacing a spurious isStale flag or older arrivals until the next poll corrected it (≤60s). The map snapshot lastLoaded() was already protected by the repository's compareAndSet (#1932); this closes the same gap one layer up, in the ViewModel's `loaded` StateFlow. Fix: a monotonic refreshGeneration bumped at the start of each refresh; a completion applies its result only while it's still the latest, else it drops out (no state write, no map-snapshot emit). All reads/writes are confined to the ViewModel dispatcher — the only suspension in refresh() is the fetch — so a plain Int needs no synchronization. Test drives two overlapping refreshes (per-call gates on the fake repository) completing out of order and asserts the older-started stale completion cannot overwrite the fresher one; verified red without the guard. Fixes #1933 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesArrivals refresh latest-wins handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant RefreshOne
participant RefreshTwo
participant ArrivalsRepository
participant ArrivalsViewModel
RefreshOne->>ArrivalsViewModel: start older refresh
RefreshTwo->>ArrivalsViewModel: start newer refresh
ArrivalsRepository-->>RefreshTwo: return fresh result
ArrivalsViewModel->>ArrivalsViewModel: apply newer generation
ArrivalsRepository-->>RefreshOne: return stale result
ArrivalsViewModel->>ArrivalsViewModel: drop superseded completion
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/ArrivalsViewModel.kt`:
- Around line 127-130: Move the lastResponseTime = WallTime.now() assignment in
the refresh completion flow to after the generation != refreshGeneration
early-return guard. Ensure superseded requests do not update the poll timer,
while valid completions still record their completion time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f967462-1071-45d4-9d63-14908e66e5de
📒 Files selected for processing (2)
onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/ArrivalsViewModel.ktonebusaway-android/src/test/java/org/onebusaway/android/ui/arrivals/ArrivalsViewModelTest.kt
Move the lastResponseTime stamp after the latest-wins guard so a superseded (later-completing) refresh no longer pushes the poll timer past the fresher completion's timestamp — the poll cadence now measures its 60s interval from the shown data's arrival. Addresses review feedback on #1934. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #1933 (split out from a CodeRabbit finding on #1932, where it was out of scope).
Problem
ArrivalsViewModel.refresh()applied every completed result unconditionally (loaded.value = data). The 60s poll loop awaits itssuspend refresh()sequentially, so it never overlaps itself — butmanualRefresh()andloadMore()eachviewModelScope.launch { refresh() }. So a user refresh (toolbar button, or the alert-dialog dismiss inArrivalActionHandlers) can run concurrently with an in-flight poll and finish out of order: the older-started completion'sloaded.value = datathen clobbers the fresher result.Impact (low, self-correcting): a spurious
isStaleflag (an overlapping call whose own network failed returns the repo's stale fallback) or momentarily older arrivals, until the next poll corrects it (≤60s). The map snapshotlastLoaded()was already protected by the repository'scompareAndSet(#1932); this closes the same gap one layer up, in the ViewModel'sloadedStateFlow.Fix
A monotonic
refreshGeneration, bumped at the start of eachrefresh(). A completion applies its result only while it's still the latest; otherwise it drops out — no state write, no map-snapshot emit:All reads/writes are confined to the ViewModel dispatcher (the only suspension in
refresh()is the fetch), so a plainIntneeds no synchronization. Chose the generation counter over serializing/cancelling refreshes because it preserves the existing parallelism and is the least behavior-changing — only the superseded completion's application is dropped.Test
Drives two overlapping refreshes (per-call gates added to the fake repository) completing out of order and asserts the older-started stale completion cannot overwrite the fresher one (checks both
isStaleandwindowEnd). Confirmed red without the guard (only that test fails) and green with it.Verification
ArrivalsViewModelTestcases pass; full unit suites green onobaGoogleandobaMaplibrewith-PwarningsAsErrors=true; compiles clean.🤖 Generated with Claude Code
Summary by CodeRabbit