Repository navigation
Conversation
5e70e90 to
54d9cc0
Compare
32b3733 to
9306e4f
Compare
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
9306e4f to
db151fe
Compare
db6a92f to
a8e806b
Compare
|
Assignment here @chuckdries Thanks! |
a8e806b to
b7751b0
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
4e82545 to
0905b80
Compare
8881b39 to
cb8bdd5
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
fabioh8010
left a comment
There was a problem hiding this comment.
@LukasMod Could you write and verify more manual tests for tab syncing? Just to make sure the change is safe.
|
@fabioh8010 I added 4 more tests, looks okay. I didn't spot issues |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb8bdd5c6d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
Bump @chuckdries on this one #884 (comment) as we are coming from follow-up of this review #863 (review) Thanks! |
|
I'm stealing the review on this one since I have context on the prior PRs and the project |
|
@TaduJR I went with your version, since my main goal was to reduce the false-positive logs from multi-tab sync. I agree the race you found needs a separate PR 👍 I took your commit as is (40d1c56). It:
I re-ran the manual tests and couldn't reproduce those logs anymore. Thanks for your help on this one! PR with onyx pin updated too: Expensify/App#105902 |
TaduJR
left a comment
There was a problem hiding this comment.
This looks good to me.
A few optional nits inline, none of which changes behaviour. The one worth keeping is the test: Onyx.clear must not reset the mark on a key it keeps, or a removal and write-back from another tab that straddle the clear logs the false alarm again.
| @@ -320,6 +339,7 @@ class OnyxCache { | |||
| } | |||
| } else { | |||
| this.nullishStorageKeys.delete(key); | |||
There was a problem hiding this comment.
| this.nullishStorageKeys.delete(key); | |
| this.forgetEmptyMarks(key); |
| @@ -249,6 +267,7 @@ class OnyxCache { | |||
| } | |||
| } else { | |||
| this.nullishStorageKeys.delete(key); | |||
There was a problem hiding this comment.
| this.nullishStorageKeys.delete(key); | |
| this.forgetEmptyMarks(key); |
| @@ -173,6 +189,7 @@ class OnyxCache { | |||
| // When a key is explicitly set in cache, we can remove it from the list of nullish keys, | |||
| // since it will either be set to a non nullish value or removed from the cache completely. | |||
| this.nullishStorageKeys.delete(key); | |||
There was a problem hiding this comment.
| this.nullishStorageKeys.delete(key); | |
| this.forgetEmptyMarks(key); |
| this.removedInOtherInstanceKeys.add(key); | ||
| } | ||
|
|
||
| /** Whether another instance removed this key and it hasn't changed here since */ |
There was a problem hiding this comment.
set, hydrate and merge clear the two sets together by hand. A small helper keeps them in step; the next three suggestions use it.
| /** Whether another instance removed this key and it hasn't changed here since */ | |
| /** Whether another instance removed this key and it hasn't changed here since */ | |
| hasRemovedInOtherInstanceKey(key: OnyxKey): boolean { | |
| return this.removedInOtherInstanceKeys.has(key); | |
| } | |
| /** A write here replaces what the cache knew about the key being empty, including another instance's removal */ | |
| private forgetEmptyMarks(key: OnyxKey): void { | |
| this.nullishStorageKeys.delete(key); | |
| this.removedInOtherInstanceKeys.delete(key); | |
| } |
| @@ -79,6 +79,10 @@ function init({ | |||
| // Capture the previous cached value before cache.set() so notifyCollection() can diff old vs new per member. | |||
| const previousValue = isCollectionMember ? cache.get(key) : undefined; | |||
| cache.set(key, value); | |||
There was a problem hiding this comment.
Nit: this comment explains the cache.set (why the key stays indexed), so it reads better above it.
| cache.set(key, value); | |
| // The key stays indexed because this answer can predate a write of ours still on its way to storage, and only indexed keys get deleted by Onyx.clear and collection sweeps. | |
| cache.set(key, value); |
|
|
||
| // The handler Onyx.init registered for changes another tab wrote to storage. | ||
| const getSyncCallback = (): ((pairs: Array<[OnyxKey, unknown]>) => void) => { | ||
| const syncCallback = (StorageMock.keepInstancesSync as jest.Mock).mock.calls.at(-1)?.[0] as ((pairs: Array<[OnyxKey, unknown]>) => void) | undefined; |
There was a problem hiding this comment.
Nit: these sync tests rely on shouldSyncMultipleInstances defaulting from global.localStorage. Passing shouldSyncMultipleInstances: true in this file's Onyx.init, and in useOnyxTest.ts, would make that explicit.
Details
Follow-up to #863
This PR clears the noise from
[Onyx] Cache held no value for a key storage still has, so the lines left in the logs point at real cache misses.Production logs about 7,200 of these lines per working day (average of the five weekdays Oct 28 to Nov 3), all from web browsers. I ran two tabs with extra logging and traced every hit to the other tab removing a key and writing it back, or removing it twice. None of them was a lost value.
On web, the sync handler applies a key removed in another tab as
cache.set(key, undefined), so the key stays indexed with no value. The cache-miss diagnostic from #863 reads "indexed, no value" as a lost value. It then reads storage, logs if the other tab has already written the key back, and uses up the collection's one report for the session, which can hide a later real miss in that collection.The sync handler still calls
cache.set(key, undefined)as onmain, and now also marks the key as removed in another tab. The diagnostic skips marked keys. Any change to the key in this tab (set,merge,hydrate,drop) clears the mark, so a later real miss on that key, or on another member of its collection, still logs.Cache values, the key index, the collection member registry and subscriber notifications behave as on
main. Only the diagnostic changes.Out of scope: a race in the sync receiver. It reads a key from disk and applies what it finds, so a removal answer can be older than a write this tab still has on its way to storage. That race exists on
mainand this PR leaves it alone. The diagnostic also skips the marked key in that case, so these logs won't tell us whether the race happensRelated Issues
Expensify/App#101910
Follow-up to #863
Linked E/App PR
Expensify/App#105902
Automated Tests
tests/unit/OnyxSubscriptionManagerTest.ts, cache miss reporting:tests/unit/onyxCacheTest.tsx: the mark doesn't count as cached, andset,merge,hydrateanddropclear it.tests/unit/onyxTest.ts, stale removal synced from another instance:Onyx.clearstill deletes the row,mergeCollectionmerges instead of overwriting,multiSetandmergeCollectionwith null delete the row, andsetCollectiondeletes a member it leaves out. These all matchmainand fail if synced removals take the key out of the index again.tests/unit/useOnyxTest.ts:useOnyxon a key another tab removed, known-empty or not, reportsloadingwhile a merge is pending and then the merged value.Full suite: 787 tests passing. Lint and
tsc --noEmitclean.Manual Tests
Web, two tabs on one account, console open with Verbose level. In every test, neither tab should log
[Onyx] Cache held no value for a key storage still has.Basic flow
Remove then re-add the same key
Collection shrinks from the other tab
Offline queue across tabs
Expense created and deleted from the other tab
Removal while the other tab is writing the same key
Sign-out during the other tab's reload
Author Checklist
### Related Issuessection above### Linked E/App PRsection above, and verified this change against it (E/App CI passed and manual testing completed)TestssectiontoggleReportand 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
N/A
Android: mWeb Chrome
android-web.mov
iOS: Native
N/A
iOS: mWeb Safari
ios-web.mov
MacOS: Chrome / Safari
web.mov
test.2.mov
test3.mov
test4-5.mov
signout.mov