-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix for the IllegalArgumentException required value was null crash #13191
Fix for the IllegalArgumentException required value was null crash #13191
Conversation
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #13191 +/- ##
============================================
+ Coverage 40.62% 40.64% +0.01%
+ Complexity 6372 6368 -4
============================================
Files 1345 1345
Lines 77271 77322 +51
Branches 10608 10612 +4
============================================
+ Hits 31395 31429 +34
- Misses 43115 43130 +15
- Partials 2761 2763 +2 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the improvement @kidinov. I tested the order detail screen and couldn't reproduce any crash either.
Here are my observations regarding the new approach:
- If
_order
starts with null, consumers relying onawaitOrder
will suspend until a valid value is provided. While this behaviour is intentional, we should ensure that all paths callingawaitOrder
account for this suspension and avoid deadlocks or unexpected delays.
What do you think about doing something like this? Is it worth it?
suspend fun awaitOrderWithTimeout(timeoutMillis: Long): Order {
return withTimeout(timeoutMillis) { _order.filterNotNull().first() }
}
- The
viewState
update logic in updateOrder is decoupled from_order
. If_order
is updated outsideupdateOrder
(e.g., directly emitting a new value),viewState
might become inconsistent with the_order
state.
We have already taken care of this by making _order
private and just exposing updateOrder
method to outside class. However, this still doesn't prevent from someone using _order
directly inside the view model itself. Probably, commenting on the updateOrder
method to just use that method to update the order and nothing else would help create awareness?
- Use a synchronized block or atomic operation to ensure
_order
andviewState
are updated atomically?
@Synchronized
fun updateOrder(newOrder: Order) {
...
}
- If multiple coroutines simultaneously call awaitOrder before an Order is available, they will all suspend. Once an Order is emitted, they will resume at the same time. Right?
We could limit simultaneous collectors by using a mechanism like StateIn
to share the state between multiple coroutines and avoid redundant suspensions:
private val _orderFlow = MutableStateFlow<Order?>(null)
val orderFlow: StateFlow<Order?> = _orderFlow.filterNotNull()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = null
)
fun updateOrder(newOrder: Order) {
_orderFlow.value = newOrder
}
All these are just my observations and I'm mostly interested in hearing your thoughts more than any changes to the codebase.
@@ -939,7 +939,7 @@ class OrderDetailViewModelTest : BaseUnitTest() { | |||
if (it is ShowSnackbar) snackbar = it | |||
} | |||
|
|||
viewModel.order = order | |||
viewModel.updateOrder(order) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ What do you think about writing test that ensures both _order and viewState
are updated when we call updateOrder
method?
Something like below
@Test
fun `updateOrder updates both _order and viewState correctly`() {
val newOrder = Order(id = 1, ...)
viewModel.updateOrder(newOrder)
assertEquals(newOrder, viewModel.order.value)
assertEquals(newOrder, viewModel.viewState.orderInfo?.order)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly on this case but I added a test on the update logic
Hey @AnirudhBhat. Thanks for the review valid points
Could you please explain a bit on this idea? My understanding is that after the timeout is gone, we will just crash with
Good point! I thought about the same problem. Changed the approach to make it impossible to update the order without updating the state
That method is gone. Now, it's not transactional (is this what you mean by atomically?), but I don't think that this is a problem, or its? Wdyt?
Sorry, I’m not following this one. Could you please explain it a bit more? How can we avoid redundant suspensions? If no order has been fetched yet, then everyone who accesses that order field must wait (suspend) until it's available. How does the flow helps with redundancy? |
Yes, you’re absolutely correct. The idea behind using If a bug or unexpected behaviour prevents an Order from being emitted, the timeout provides a fallback mechanism.
Awesome. Thanks!
Yes. By “atomic,” I meant ensuring that
The idea behind using |
Generated by 🚫 Danger |
Yeah, good call. I was thinking about what timeout it should be, and end up with three seconds. As per the current code, it looks like we even fetched the order from the server if it's locally not stored, but before the change, that would always be a crash, as clearly the order field would be accessed by then. Now, it's not the case, but I guess even 3 seconds of showing nothing on the screen maybe too much and we should crash. If this happens we will fix this behavior. wdyt? Maybe set the timeout even lower? |
A 3-second timeout seems reasonable in most cases. If we set a short timeout (e.g., 1-2 seconds) and the user is on a slow or unstable connection, it might prematurely lead to a crash or fallback behaviour. In such cases:
I guess 3 second timeout should cover best and worst (slow internet) cases? WDYT? |
@@ -993,6 +992,13 @@ class OrderDetailViewModel @Inject constructor( | |||
} | |||
} | |||
|
|||
suspend fun awaitOrder(): Order = | |||
withTimeout( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Do you think its worth writing test to ensure the logic crash after 3 seconds of suspension with not result?
@Test
fun `awaitOrder should throw TimeoutCancellationException after 3 seconds`() = testBlocking {
val exception = assertThrows(TimeoutCancellationException::class.java) {
awaitOrder()
}
}
I realise that there's no point in testing withTimeout
function as the Kotlin team has already done it. My thought process is that the test explicitly documents that the function is expected to crash with a TimeoutCancellationException
if no value is emitted within the specified time. It helps clarify the intended behaviour of the function to future developers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a test here 0cca9c0
The thing is that remote fetch now causes the crash if I understand correctly, as if the order is not in the VM property by the time everyone accesses it - there is a crash. So, it looks like there is an assumption that the order must be present locally in the DB. BUT At the same time, there is this code
Which, iic doesn't make sense, and that should just crash anyway. Maybe this is only for the cases when the view was recreated, and somehow the order is gone from the database but still in the memory in the VM - no idea. The timeout may not be measured based on the network call duration. If it would, I would put it much bigger than 3s, probably 30s as I believe our network timeout wdyt? |
@kidinov Thanks for explaining! I see the point now. In that case, your below suggestion sounds good to me.
I think 3 seconds works well for now. We can monitor the impact of this change and decide later if further refinements are needed. |
Closes: #13178
Description
There has been a fundamentally flawed approach regarding the manner in which an order is fetched and accessed. The Order is retrieved asynchronously, while they can be accessed in anytime, without waiting if it's retireved or. This discrepancy may, in certain instances, lead to accessing fields before they have been initialized. Furthermore, there are instances where view model methods directly return data to the fragment, complicating the resolution of these issues.
The PR makes accessing the order via the suspend function, so if the order has not yet been retrieved, then the caller side will be waiting for it.
As mentioned above the whole structure of the fragment and VM brings challenges and without deep refactoring the solution also brings downsides, like using lifecycle scope to access things in VM
I am frankly not sure if the solution is better than the problem as this probably also may create another unforeseen issue. @AnirudhBhat @samiuelson wdyt?
Steps to reproduce
I don't know how to reproduce the crash, it's race condition
Testing information
Try to validate that order details functionality is not broken as much as possible
The tests that have been performed
Above
Images/gif
RELEASE-NOTES.txt
if necessary. Use the "[Internal]" label for non-user-facing changes.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: