-
Notifications
You must be signed in to change notification settings - Fork 83
Update error handling in Onyx to prevent unnecessary data eviction #694
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
base: main
Are you sure you want to change the base?
Update error handling in Onyx to prevent unnecessary data eviction #694
Conversation
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 looks good to me, i just have a few minor comments 🙌🏼
lib/OnyxUtils.ts
Outdated
| * - Other errors: retries the operation | ||
| */ | ||
| function evictStorageAndRetry<TMethod extends typeof Onyx.set | typeof Onyx.multiSet | typeof Onyx.mergeCollection | typeof Onyx.setCollection>( | ||
| function retryOperation<TMethod extends typeof Onyx.set | typeof Onyx.multiSet | typeof Onyx.mergeCollection | typeof Onyx.setCollection>( |
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.
NAB: Do you think it would be feasible to add a type backing TMethod that requires an onyx method to have an optional retryAttempt parameter on the last position? 👀
| const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => storageError === error?.name?.toLowerCase() || errorMessage?.includes(storageError)); | ||
|
|
||
| if (!isStorageCapacityError) { | ||
| // @ts-expect-error No overload matches this call. |
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.
My above comment was to prevent the need for this // @ts-expect-error comment
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 // @ts-expect-error comment isn't coming due to retryAttempt parameter, it something that exists on main as well, just cause onyx methods are different and can't be overloaded.
react-native-onyx/lib/OnyxUtils.ts
Lines 898 to 899 in 6a24680
| // @ts-expect-error No overload matches this call. | |
| return remove(keyForRemoval).then(() => onyxMethod(...args)); |
I've tried to go with different approaches (add retryOperation overloads, adding assertion worked by didn' look much better)
evictStorageAndRetry to not evict the data if error isn't storage related| const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged); | ||
|
|
||
| // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. | ||
| if (!hasChanged && !retryAttempt) { |
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've updated the condition to not skip Storage.setItem operation if the retryAttempt is defined.
I haven't added any cache-clearing logic, as I'm not sure about it, since:
- currently app doesn't rollback the cache in case of a failed operation (for any operation)
- the subscribers (useOnyx for example) are also notified with the updated values no matter if storage operation succeed or not
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 think it makes sense. Could we change the comment above to reflect the new logic?
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.
Minor things!
@VickyStash is possible to add some unit tests to cover these retry mechanisms?
| const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged); | ||
|
|
||
| // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. | ||
| if (!hasChanged && !retryAttempt) { |
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 think it makes sense. Could we change the comment above to reflect the new logic?
Sure, it's planned! I've mentioned it here. |
|
While writing the tests, I've found some flaws in the eviction mechanism. We get react-native-onyx/lib/OnyxCache.ts Lines 419 to 429 in 6a24680
In the So if we react-native-onyx/lib/OnyxUtils.ts Line 899 in 6a24680
We notify subscribers about that => the key is added to the recentlyAccessedKeys.
It means:
|
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.
🚀
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.
@VickyStash Did you do anything about those eviction problems you found while writing the tests? Is that something we need to fix here in this PR or is that going to be something we look at separately?
| <dd><p>If we fail to set or merge we must handle this by | ||
| evicting some data from Onyx and then retrying to do | ||
| whatever it is we attempted to do.</p> | ||
| <dt><a href="#retryOperation">retryOperation()</a></dt> |
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.
Did you regenerate these docs after your recent changes? I don't think retryOperation() is exposed anywhere publicly, right?
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.
@tgolen I've re-generated it one more time.
Yeah, it's not public! This file is also API-INTERNAL (describes internal methods)
I haven't added any changes for it. I've the idea of the fix in mind: updating this check to check not only for react-native-onyx/lib/OnyxUtils.ts Lines 692 to 697 in 6a24680
This way we at least won't try to evict what we know is removed. |
|
I think as long as that issue doesn't prevent this PR from working, then I think we should tackle it separately. I think the best step would be to make a proposal in the #quality channel in Slack. |
| const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); | ||
|
|
||
| return Storage.multiSet(keyValuePairs) | ||
| .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) |
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.
same as #694 (comment)
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.
@abzokhattab Onyx.setCollection/setCollectionWithRetry doesn't have any options param, right?
So there is nothing extra to pass.
lib/types.ts
Outdated
| isProcessingCollectionUpdate?: boolean; | ||
| }; | ||
|
|
||
| type OnyxRetryOperation = |
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.
NAB: Minor request, but OnyxRetryOperation sounds to me as if these operations would mainly retry and if that was their main purpose. I would name this type something like RetriableOperation or RetriableOnyxOperation
tests/unit/onyxUtilsTest.ts
Outdated
|
|
||
| describe('retryOperation', () => { | ||
| it('should retry only one time if the operation is firstly failed and then passed', async () => { | ||
| const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); |
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.
Can we move this line out of the individual tests and store globally?
tests/unit/onyxUtilsTest.ts
Outdated
| describe('retryOperation', () => { | ||
| it('should retry only one time if the operation is firstly failed and then passed', async () => { | ||
| const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); | ||
| const genericError = new Error('Generic storage error'); |
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.
Same for this actually
tests/unit/onyxUtilsTest.ts
Outdated
|
|
||
| it('should stop retrying after MAX_STORAGE_OPERATION_RETRY_ATTEMPTS retries for failing operation', async () => { | ||
| const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); | ||
| const genericError = new Error('Generic storage error'); |
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.
Same here
tests/unit/onyxUtilsTest.ts
Outdated
|
|
||
| it('should not retry in case of storage capacity error and no keys to evict', async () => { | ||
| const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); | ||
| const quotaError = new Error('out of memory'); |
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.
And here :)
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.
LGTM, i have minor requests but these are not blockers. Thanks for your work @VickyStash 🙌🏼
Details
This PR updates error handling by:
Related Issues
Expensify/App#73779
Automated Tests
Added to
tests/unit/onyxUtilsTest.tsManual Tests
Before

After
Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
MacOS: Desktop