Skip to content
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 race condition when calling mutate synchronously #735

Merged
merged 5 commits into from
Nov 5, 2020

Conversation

shuding
Copy link
Member

@shuding shuding commented Oct 30, 2020

Bug

We have this inconsistency issue in current version:

// we call `mutate` twice, synchronously, with a callback that reads the latest value
// let's assume the initial data is `0`

mutate('key', v => v + 1, false)
mutate('key', v => v + 1, false)

The data should be 2 after these 2 synchronous mutation calls. However if you test, it is actually 1 (here's a great bug reproduction from @derindutz).

The cause is that we are always updating the cache in async (as #619 tries to fix), so the argument v passed to the second mutation callback is still 0. To make it clear, here's a diagram addressing this bug:

image

Solution

We at first tried to make the mutate process synchronous (if the callback is not a promise or async function), but that brings another issue which is intermediate state:

mutate('key', v => v + 1, false)
mutate('key', v => v + 1, false)

Now it correctly renders 2 at the end, but there's a moment that 1 was rendered before that. Instead we should "batch"
synchronous mutations. It is because that the first mutation can't get deduplicated by the second one anymore. They both render the data.

image

To ensure the correctness of mutate, we need to make state broadcasting always asynchronous (kinda like React), but cache needs to be updated ASAP. So the latter mutations will always have the result from the former, but only the last mutation will get rendered:

image

The consequence of this fix is, if you mutate something and immediately check the result, it won't be updated. So in our test I added await 0 to delay the check in the next tick:

mutate('mutate-3', 'off', false)
await 0 // if no `await` here, it's gonna be wrong
expect(container.textContent).toMatchInlineSnapshot(`"off"`)

Fixes #566. Related to #619.

src/use-swr.ts Outdated Show resolved Hide resolved
src/use-swr.ts Outdated Show resolved Hide resolved
src/use-swr.ts Outdated Show resolved Hide resolved
@shuding
Copy link
Member Author

shuding commented Oct 30, 2020

Great feedback @huozhi, thanks!

Copy link
Member

@huozhi huozhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! awesome work and explanation there 👍


if (_data && typeof _data === 'function') {
// `_data` is a function, call it passing current cache value
try {
data = await _data(cache.get(key))
_data = _data(cache.get(key))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can keep the previous 'else if' flow here instead of reassign _data here ?

if (_data && typeof _data === 'function') {
    // `_data` is a function, call it passing current cache value
    try {
      data = _data(cache.get(key))
    } catch (err) {
      error = err
    }
  } else if (_data && typeof _data.then === 'function') {
    // `_data` is a promise
    isAsyncMutation = true
    try {
      data = await _data
    } catch (err) {
      error = err
    }
  } else {
    data = _data
  }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can’t. If _data is an async function, calling it without await will get a promise returned, and we have to resolve it again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On i see now 😅. I think i can add a test for mutate with async function. we don't have one now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@promer94 that would be great, thanks!

@shuding
Copy link
Member Author

shuding commented Nov 5, 2020

Tested a bit, should be fine to ship this :)

@shuding shuding merged commit 5dbd6d5 into master Nov 5, 2020
@shuding shuding deleted the fix-mutate-race branch November 5, 2020 16:17
shuding added a commit that referenced this pull request Nov 13, 2020
* 0.3.8

* replace rIC with rAF (#744)

* Fix race condition when calling mutate synchronously (#735)

* fix race condition when calling mutate synchronously

* fix test

* add comment

* fix code reviews

* refactor: support SSR in Deno (#754)

* refactor: support SSR in Deno

* refactor: improve Deno determining

* Add @ts-ignore

Co-authored-by: Shu Ding <g@shud.in>

Co-authored-by: Shu Ding <g@shud.in>

* update useSWR-loading and useSWR refresh

* fix eslint error (#768)

Co-authored-by: Shu Ding <g@shud.in>
Co-authored-by: X <git@iamje.com>
shuding added a commit that referenced this pull request Dec 16, 2020
* 0.3.8

* replace rIC with rAF (#744)

* Fix race condition when calling mutate synchronously (#735)

* fix race condition when calling mutate synchronously

* fix test

* add comment

* fix code reviews

* refactor: support SSR in Deno (#754)

* refactor: support SSR in Deno

* refactor: improve Deno determining

* Add @ts-ignore

Co-authored-by: Shu Ding <g@shud.in>

Co-authored-by: Shu Ding <g@shud.in>

* fix eslint error (#768)

* Fix `mutateCallback` types (#745)

* Fix `mutateCallback` types

* WIP

* Add CodeSandbox CI (#769)

* add CodeSandbox CI

* add new line

* fix install cmd

Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com>

* dispatch's payload type is actionType and run lint (#772)

* chore: payload is actionType

* chore: move a ts-ignore comment

* Fix suspense (#777)

* fix #494

* add comment

* rename to initialMountedRef

* 0.3.9

* fix: mark isValidating as false when key is falsy (#757)

* fix: tear down when key turns to empty

* use false for empty key

* Fix README.md typo (#783)

'/api/data' => '/api/user' in "Multiple Arguments"

* fix: do mount check in config callback (#787)

* Update api-hooks example README.md (#790)

Updated the Vercel deploy link to the correct directory

* Return '@null' if args is null ASAP (#767)

* chore: return 'null' if arg[i] is null ASAP

* chore: update comment

* chore: use continue

* Bump ini from 1.3.5 to 1.3.8 (#806)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](npm/ini@v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* update test

Co-authored-by: Shu Ding <g@shud.in>
Co-authored-by: X <git@iamje.com>
Co-authored-by: Umidbek Karimov <uma.karimov@gmail.com>
Co-authored-by: Paco <34928425+pacocoursey@users.noreply.github.com>
Co-authored-by: matamatanot <39780486+matamatanot@users.noreply.github.com>
Co-authored-by: Jiachi Liu <inbox@huozhi.im>
Co-authored-by: sAy <47605337+mingcenwei@users.noreply.github.com>
Co-authored-by: William Crutchfield <william.r.crutchfield@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Race condition when mutate is called multiple times in short succession with the same key
3 participants