-
Notifications
You must be signed in to change notification settings - Fork 73
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 concurrent writes in the update operation #384
Conversation
expect(otherTestCallback).toHaveBeenNthCalledWith(1, 'pizza', ONYX_KEYS.OTHER_TEST); | ||
expect(collectionCallback).toHaveBeenNthCalledWith(2, {[itemKey]: {a: 'a'}}); | ||
expect(testCallback).toHaveBeenNthCalledWith(2, 'taco', ONYX_KEYS.TEST_KEY); | ||
expect(otherTestCallback).toHaveBeenNthCalledWith(2, 'pizza', ONYX_KEYS.OTHER_TEST); |
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.
This previously was receiving the write updates before the hydration callback, but now that the write operations take a little longer the order is correct.
}); | ||
|
||
return clearPromise.then(() => Promise.all(_.map(promises, p => p()))); | ||
return innerUpdate(data); |
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.
As a data point, I tried the proposed solution by Marc which would try to resolve earlier while keeping the writes async, this however is not correct and broke a bunch of other tests.
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.
Has there been any testing done in App to see what kind of performance impact this will have? If so, can you please attach it to this PR?
lib/Onyx.js
Outdated
@@ -1534,6 +1539,7 @@ const Onyx = { | |||
connect, | |||
disconnect, | |||
set, | |||
get, |
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.
It was pretty import to avoid exporting this so that people don't get lazy in App and start using it all over the place (which encourages a lot of chaining calls). It looks like you're exporting it for use in the tests and I would like to propose these alternatives:
- Inside the tests use
Onyx.connect()
to get the value (this is the preferred method) - Rename this export to something like
onlyToBeUsedInTestCode_get
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.
From what I saw Onyx.connect has caching implemented, which might not be what is actually persisted on the DB. I was about to directly query the DB which is at the end of the day what really matters. I think this get
might also use caches right? if so, checking the DB should be the correct path.
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.
I like the idea of querying the DB directly if that's possible.
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.
Done
No, I haven't tested the app. I can try to get some metrics but I already asked in slack if there is any particular call to update that is heavy. |
The |
OK, I just created a large account for testing things like this, so let me give it a try with this today. |
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.
I tested it in App with a large account and I couldn't notice any performance impact, so I think these are good changes. I just noticed this one thing.
@@ -1392,6 +1392,36 @@ function mergeCollection(collectionKey, collection) { | |||
}); | |||
} | |||
|
|||
function innerUpdate(data) { |
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.
Please add JSDoc
Doing some testing now... |
Alright well - it is defying my expectations a bit, but when timing the calls to Sign in trialsWith sync updates: 1 Timing:development.new.expensify.flushQueue 458 2 Timing:development.new.expensify.flushQueue 468 3 Timing:development.new.expensify.flushQueue 311 Without sync updates (but on latest 1 Timing:development.new.expensify.flushQueue 488 2 Timing:development.new.expensify.flushQueue 420 3 Timing:development.new.expensify.flushQueue 464 This was a web-only test. Haven't tested on iOS or Android - but looks promising. |
} | ||
}); | ||
|
||
return clearPromise.then(() => Promise.all(_.map(promises, p => p()))); |
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.
I see we are undoing the previous logic to move the clear()
to the front of the update queue. Does running the updates sync solve the issue that caused us to make that change?
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.
ah, probably not. I'll take care of it.
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.
I'm not sure making sure clear runs first is a desired behavior but I guess now it is too late to change it, other parts of the system might rely on it. I added sorting to the list to make sure clear operations get executed first.
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.
Yeah, I agree it is a bit of a weird hack. But changing it could have unexpected consequences. Thanks for updating.
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.
🚀
switch (onyxMethod) { | ||
case METHOD.SET: | ||
promise = set(key, value); | ||
break; | ||
case METHOD.MERGE: | ||
promise = merge(key, value); | ||
break; | ||
case METHOD.MERGE_COLLECTION: | ||
promise = mergeCollection(key, value); | ||
break; | ||
case METHOD.MULTI_SET: | ||
promise = multiSet(value); | ||
break; | ||
case METHOD.CLEAR: | ||
promise = clear(); | ||
break; | ||
default: | ||
break; | ||
} |
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.
It looks like this change possibly led to some regressions spotted in this PR and discussed in this Slack thread that updated the Onyx version
Details
The update operation was written in such manner that it fired all database operations at once. Some of the operations internally rely on other promises, meaning they will get re-scheduled on another tick. This causes unpredictable behavior as you cannot guarantee the order of the operations. The solution is clear and is the only one, all DB writes must respect the order of the operations.
The incorrect ordering of the operations being committed to the DB causes other random behavior around the app as well. Linked the two issues.
Related Issues
Expensify/App#28737
Expensify/App#28584
Automated Tests
Added one more test that makes sure the correct data is persisted. It failed with the previous implementation, now it passes.
Linked PRs