fix(ui): eliminate race condition between refreshDevices and selectDevice - #41
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 13 |
| Duplication | 2 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
🤖 FlashCore Autonomous AI PR Review📋 Overview & IntentThis pull request aims to eliminate a race condition between device list refreshes ( 🚦 VerdictCHANGES REQUESTED While the addition of 🔍 Critical & High Priority Findings1. FSM & UI State Desynchronization Race Condition
// app/src/main/java/com/ashishsinghbora/flashcore/ui/FlasherViewModel.kt
_uiState.update { current ->
val currentDev = current.selectedDevice
val activeSelected = when {
autoSelectDevice != null -> diskList.find { it.device == autoSelectDevice } ?: diskList.firstOrNull()
currentDev != null && currentDev.device == null -> currentDev
currentDev != null -> diskList.firstOrNull { isSameDevice(it, currentDev) } ?: diskList.firstOrNull()
else -> diskList.firstOrNull()
}
current.copy(
connectedDevices = diskList,
selectedDevice = activeSelected
)
}
val activeSelected = _uiState.value.selectedDevice
if (diskList.isNotEmpty() && activeSelected != null) {
fsm.transition(FlasherEvent.DevicesUpdated(diskList, activeSelected))
} else if (_uiState.value.selectedDevice?.device != null) {
fsm.transition(FlasherEvent.ResetToIdle)
}
_uiState.update { current ->
current.copy(fsmState = fsm.state.value)
}💡 Optimizations & Idiomatic Kotlin Suggestions
Reviewed autonomously by |
There was a problem hiding this comment.
Pull Request Overview
The pull request successfully addresses the race condition by moving device evaluation into the _uiState.update block, but the implementation introduces two high-risk issues that should prevent merging in its current state.
First, performing side effects like fsm.transition inside the update block is dangerous because the block may execute multiple times under contention, leading to duplicate or inconsistent state transitions. Second, a logic regression was introduced that prevents the State Machine from transitioning to ResetToIdle when hardware is disconnected if a selection was previously active. This violates the requirement for the UI to handle hardware removal gracefully.
While Codacy reports the PR as 'up to standards', these architectural and logic issues need to be resolved to ensure stability.
About this PR
- The revised logic may leave the State Machine in an inconsistent 'Ready' state even after hardware is disconnected, because the transition to
ResetToIdleis now bypassed if a previous selection existed. This breaks the expected behavior for hardware detachment.
Test suggestions
- Verify that refreshDevices preserves a manually selected device if it is still present in the updated disk list.
- Verify that refreshDevices preserves a mock device (device == null) even if the hardware disk list is empty.
- Verify that the FSM transitions to ResetToIdle when the disk list becomes empty and no device was previously selected.
- Check that autoSelectDevice parameter takes precedence over existing selection logic within the atomic update block.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify that refreshDevices preserves a manually selected device if it is still present in the updated disk list.
2. Verify that refreshDevices preserves a mock device (device == null) even if the hardware disk list is empty.
3. Verify that the FSM transitions to ResetToIdle when the disk list becomes empty and no device was previously selected.
4. Check that autoSelectDevice parameter takes precedence over existing selection logic within the atomic update block.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| } else if (selected == null && currentSelected == null) { | ||
| fsm.transition(FlasherEvent.ResetToIdle) | ||
| } |
There was a problem hiding this comment.
🔴 HIGH RISK
This condition introduces a logic regression. By adding 'currentSelected == null', the FSM will no longer transition to ResetToIdle if the hardware is unplugged while a device was selected. If the device is no longer in the diskList, the FSM should be reset regardless of the previous selection state.
| } else if (selected == null && currentSelected == null) { | |
| fsm.transition(FlasherEvent.ResetToIdle) | |
| } | |
| } else { | |
| fsm.transition(FlasherEvent.ResetToIdle) | |
| } |
| currentSelected != null -> diskList.find { it.serialNumber == currentSelected.serialNumber } ?: if (currentSelected.device == null) currentSelected else diskList.firstOrNull() | ||
| else -> diskList.firstOrNull() | ||
| } | ||
| _uiState.update { current -> |
There was a problem hiding this comment.
🔴 HIGH RISK
Side effects such as fsm.transition(...) must be kept outside of the _uiState.update block. Because update is implemented as a compare-and-set loop, this block can be executed multiple times if there is concurrent access. This would cause the FSM to process the same event multiple times, potentially leading to invalid state transitions. Refactor the logic to calculate the new state inside the block, but trigger the transition only once after the update completes.
| } | ||
|
|
||
| override fun onCleared() { | ||
| public override fun onCleared() { |
There was a problem hiding this comment.
⚪ LOW RISK
Nitpick: The onCleared method should remain protected as defined in the ViewModel base class. Changing it to public is unrelated to the race condition fix and breaks encapsulation.
| public override fun onCleared() { | |
| protected override fun onCleared() { |
…eviewer and auditor
Description
Fixes a race condition in
FlasherViewModel.ktwhere an asynchronousrefreshDevices()coroutine evaluatingcurrentSelectedbefore_uiState.updatecould overwrite a synchronously selected device with null. Moving selection evaluation inside the atomic_uiState.updateblock ensures newly selected or mock test devices are preserved.