fix: rethrow CancellationException in AnnounceStreamViewModel#613
fix: rethrow CancellationException in AnnounceStreamViewModel#613torlando-tech merged 2 commits intomainfrom
Conversation
Fixes #608. All 9 `catch (e: Exception)` blocks were swallowing CancellationException, breaking structured concurrency. The while(true) reachable-count loop was especially dangerous — it kept running after viewModelScope cancellation, producing misleading "Job was canceled" error logs and holding references to dead ViewModel state. Add `catch (e: CancellationException) { throw e }` before each `catch (e: Exception)` block, matching the established codebase convention used in TelemetryCollectorManager, MessagingViewModel, SettingsViewModel, EventHandler, and OfflineMapDownloadViewModel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a coroutine-cancellation leak in
Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[viewModelScope launched coroutine] --> B{try block executes}
B -->|Normal completion| C[Continue / next iteration]
B -->|CancellationException thrown| D["catch (e: CancellationException)\n{ throw e } ← NEW"]
D --> E[Exception propagates up to launch]
E --> F[Coroutine / loop terminates cleanly]
B -->|Other Exception thrown| G["catch (e: Exception)\n{ Log.e(...) }"]
G --> C
style D fill:#90ee90,stroke:#228B22
style F fill:#90ee90,stroke:#228B22
Last reviewed commit: 2e49126 |
| // TODO: viewModelScope is already cancelled here; shutdown is managed | ||
| // by ColumbaApplication. Consider removing or moving to a separate scope. |
There was a problem hiding this comment.
Inaccurate TODO comment about viewModelScope lifecycle
The comment states "viewModelScope is already cancelled here", but this is not accurate. viewModelScope is backed by a CloseableCoroutineScope that is registered as a Closeable on the ViewModel. Android's ViewModel.clear() calls onCleared() first, and only cancels registered closeables (including viewModelScope) after onCleared() returns. So viewModelScope.launch { reticulumProtocol.shutdown() } on line 552 does successfully enqueue a new coroutine — but the scope is cancelled almost immediately after onCleared() returns, creating a race condition where shutdown() is very unlikely to actually execute.
The real concern is better described as: the coroutine launched here will likely be cancelled before reticulumProtocol.shutdown() gets a chance to run. The suggestion to remove or move it to a separate scope is correct, but the stated reason is misleading.
| // TODO: viewModelScope is already cancelled here; shutdown is managed | |
| // by ColumbaApplication. Consider removing or moving to a separate scope. | |
| // TODO: viewModelScope is cancelled shortly after onCleared() returns (as a | |
| // registered Closeable), so the coroutine launched below is likely cancelled | |
| // before reticulumProtocol.shutdown() executes. Shutdown is managed by | |
| // ColumbaApplication. Consider removing or moving to a separate scope. |
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/AnnounceStreamViewModel.kt
Line: 547-548
Comment:
**Inaccurate TODO comment about `viewModelScope` lifecycle**
The comment states "viewModelScope is already cancelled here", but this is not accurate. `viewModelScope` is backed by a `CloseableCoroutineScope` that is registered as a `Closeable` on the `ViewModel`. Android's `ViewModel.clear()` calls `onCleared()` **first**, and only cancels registered closeables (including `viewModelScope`) **after** `onCleared()` returns. So `viewModelScope.launch { reticulumProtocol.shutdown() }` on line 552 does successfully enqueue a new coroutine — but the scope is cancelled almost immediately after `onCleared()` returns, creating a race condition where `shutdown()` is very unlikely to actually execute.
The real concern is better described as: the coroutine launched here will likely be cancelled before `reticulumProtocol.shutdown()` gets a chance to run. The suggestion to remove or move it to a separate scope is correct, but the stated reason is misleading.
```suggestion
// TODO: viewModelScope is cancelled shortly after onCleared() returns (as a
// registered Closeable), so the coroutine launched below is likely cancelled
// before reticulumProtocol.shutdown() executes. Shutdown is managed by
// ColumbaApplication. Consider removing or moving to a separate scope.
```
How can I resolve this? If you propose a fix, please make it concise.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
viewModelScope is cancelled as a registered Closeable *after* onCleared() returns, not before — so the launch succeeds but the coroutine is likely cancelled before shutdown() executes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Fixes #608
catch (e: CancellationException) { throw e }before all 9catch (e: Exception)blocks inAnnounceStreamViewModel, matching the established codebase convention (TelemetryCollectorManager, MessagingViewModel, SettingsViewModel, EventHandler, OfflineMapDownloadViewModel)while(true)reachable-count polling loop was the most dangerous — it kept running afterviewModelScopecancellation, producing misleading "Job was canceled" error logs and holding references to dead stateonCleared()noting thatviewModelScopeis already cancelled there (separate issue)Test plan
compileNoSentryDebugKotlinbuilds successfullyAnnounceStreamViewModelTesttests pass (34 existing + 1 new)reachable count loop stops when viewModelScope is cancelledverifies the loop terminates on scope cancellation🤖 Generated with Claude Code