-
Notifications
You must be signed in to change notification settings - Fork 3k
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
[Discussion|Onyx] Mixing Onyx.set and Onyx.merge usages leads to race condition #5930
Comments
Triggered auto assignment to @NicMendonca ( |
IMO to avoid this problem we should only allow one way to update Onyx. E.g. hide But at the moment
The policy code originally used App/src/libs/actions/Policy.js Lines 267 to 269 in f66dd56
App/src/libs/actions/Policy.js Line 277 in f66dd56
|
I am not entirely sure how to triage this 😅 Adding the |
Triggered auto assignment to @bondydaa ( |
@NicMendonca This is a discussion issue and does not need to work on or exported. |
per the philosophy in the readme https://github.com/Expensify/App/#Philosophy
So why is code being merged that uses Or is the problem that even multiple |
This is a challenging one to address because it feels like a potential problem with Onyx, but also feels like a potential problem with our conventions of syncing data in New Expensify. Under normal conditions these race conditions should not be happening and the batching of But it does seem likely that when we are using a combination of It should be more clear what sort of action to take if we audit the workarounds that we suspect cause race conditions. Enforcing that |
IMO it would be best to expose only one method for updating Onyx that works similarly to merge, e.g. export interface Onyx {
update({ key: string, value: any, strategy: UpdateStrategy }): Promise<void>
// or
update(key: string, value: any, options: { strategy: UpdateStrategy }): Promise<void>
}
enum UpdateStrategy {
DeepMerge,
ShallowMerge,
Overwrite,
}
Onyx.update({ key: ONYXKEYS.TOKEN, value: Onyx.DELETE_VALUE }) This allows us to have a queue of updates similar to how merge works with a merge queue, but having an information for the type of the update e.g. overwrite or deletion would allow us to disregard any previous values in the queue since if a latter update is a complete overwrite - prior queued updates have no effect on the end result |
Nice, I like the idea of batching and having different strategies to apply changes in a synchronous way. I feel the public interface should maybe be simpler only because I'm not sure if anyone would stop to think about whether they need a deep or shallow merge 😄 It feels like Expensify/App has outgrown Onyx a bit and overtime we've run into situations where we need a more complex way to modify data. Just as a thought experiment, what if we extended the public methods Onyx has to solve some of the problems we might run into when working with data and then add the internal queue to help prevent the race conditions? Some things that have been floated in the past and in this issue...
We've discussed adding a callback to
I wonder if we could add something like Anyway, that conversation could go on for a while 😅 to refocus...
|
It's possible to go the other direction and expose a CRUD api
The most flexible solution would be something as what you've brought up: callback to Onyx.set() but for
And then it's possible to do something like Onyx.merge(ONYXKEYS.ARRAY_CONTAINING_KEY, (value) => {
return {
...value,
myNestedArray: value.myNestedArray.filter(entry => shouldKeepEntry(entry))
}
}) I guess it makes sense to have this interface for both
The
Onyx.delete(myKey, ['property', 'nested']);
// or
Onyx.delete(myKey, ['property', 'nestedList', 2])
// though it's not obvious whether remaining indexes get shifted down
This should be easy, I can post a PR
In short yes - no matter what we decide (multiple or single ways to update storage) we still need to make changes to the merge queue so that it takes into consideration overwrites that happened from updates like We just need to decide what we want to achieve - there are 2 scenarios, and only one causes a race condition Ok case - set is called first
Bug case - set is called after merge
ATM I can think of 2 ways to handle the bug case
|
The public interface can be similar to current Instead of deep and shallow merge we can stick to just 2 strategies - Onyx.update(ONYXKEYS.ARRAY_CONTAINING_KEY, (value) => {
return {
...value,
myNestedArray: value.myNestedArray.filter(entry => shouldKeepEntry(entry))
}
}) ShowcaseTypical usage (merging)Onyx.update(MY_KEY, myValue); Removing a nested keyOnyx.update(MY_KEY, { keyToDelete: Onyx.DELETE_VALUE }); Removing the whole keyOnyx.update(MY_KEY, Onyx.DELETE_VALUE); Overwriting with an API responseOnyx.update(MY_KEY, myValue, { strategy: Onyx.UpdateStrategy.SET }); Removing array items (IRL example) // If the operation failed, undo the optimistic addition
const policyDataWithoutLogin = _.clone(allPolicies[key]);
// set data back to what it was (policy), tell Onyx to discard any entries remaining past the original list
policyDataWithoutLogin.employeeList = [...policy.employeeList, Onyx.DELETE_VALUE]; |
Yeah I think we should keep things simple for now and do:
Once that is settled we'll have a better foundation to discuss other improvements - maybe by auditing the usages of |
Quick update here. I don't think there's really much else to do here just yet so I want to wrap up this discussion. I experimented with a test and proved the race condition exists. However, I'm not sure there are any actual cases where we're even calling a I think there's been some good stuff here regarding alternative methods to update Onyx data. But seems tangential to the race condition investigation so I'm gonna close this out. It would be useful to re-approach this conversation by just looking at the Policy code and determining how we can or should modify Onyx to make it work better for us. Will create a new issue for that exploration and we can reference the convos here. |
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Discussion
Decide how to deal with a race condition that happens when mixing
Onyx.merge
andOnyx.set
Problem
It's not obvious that we can introduce changes leading to
Onyx.set
andOnyx.merge
being called close in time with the same key and create a race condition.Details
During one of our PRs we discovered an issue where storage is getting overwritten with an older value due to using
Onyx.set
andOnyx.merge
to update the same key: #5726 (comment)Debugging revealed that
Onyx.merge
andOnyx.set
are called very close in time: #5726 (comment)The problematic code did this
Onyx.merge
to clear policy errorsOnyx.set
to update the same policy keymerge
from 1) completes last and overwrites local storage with older valueEven though
merge
andset
are called at the same time.merge
has to first read the full policy object in order to merge the changes to it.merge
starts andgets
the value beforeset
has updated it, since it's promise based it would continue on the next tick. In the meantimeset
saves a new value in storage (step 2). On the next tick (step 3) the merge is applied with the stale data and another call toset
overwrites the storageA debug session that captured the problem
1.New.Expensify.-.Google.Chrome.2021-10-15.17-49-48.mp4
The text was updated successfully, but these errors were encountered: