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

Chrome issue allows two tabs to acquire the lock. #1

Closed
wants to merge 2 commits into from

Conversation

latentflip
Copy link

I'm not sure that the browser test side of this PR is necessarily ready for merge, but I wanted to share it in it's current state to get a fix for this moving along.

There's a bug in fast-mutex in (at least) chrome, which allows two tabs to obtain the lock at the same time.

Chrome doesn't "commit" localStorage changes to "disk" until the current execution stack has finished. This means in the current sequence:

tab one                   |  tab two
setItem("x", "one")       |
                          |  setItem("x", "two")
getItem("x") // => "one"  |

tab one will always read "x" as one, even though tab two has changed it.

Inside fast-mutex, this allows two tabs to acquire the lock concurrently. To handle this, we need to separate set and get by a settimeout call.

To demonstrate the issue (and fix) I have created a browser test page. (Run it with npm run test-browser)

Open it in two tabs (I would recommend putting the tabs in separate windows side-by-side, otherwise chrome has a tendency to not call timeouts more frequently than 1/per second, which confuses things).

This test triggers synchronous runs every :00/:15/:30/:45 seconds past the minute in each tab. In each tab we acquire a lock, and increment a counter. When all tabs are finished, we should see the counter equal the number of open tabs, otherwise there's been a failure (either two tabs acquired the lock concurrently, or one tab failed to complete).

Here is an example of a run before the fix (in sha 67cc985):

image

With two tabs open, both acquire the lock within 1ms of each other.

image

Philip Roberts added 2 commits September 29, 2017 12:18
A sequence of synchronous calls in one page like:

setItem(x, "foo")
getItem(x)

will always return "foo", even if another tab did
a setItem(x, "bar") between the set and get.

To handle this, we need to setTimeout between calls
@latentflip
Copy link
Author

So, while this fixes chrome in the "both tabs are in front" case. I fear it may break chrome and/or other browsers where one browser is not at the forefront, as non-focussed tabs may run setTimeout(0) quite slowly, at which point the 50ms timeout in the contention resolution block is not enough.

Sigh.

@chieffancypants
Copy link
Owner

First of all, apologies on the delay. That said, if what you're saying is true, this would indeed be a huge issue. Pulling up the localStorage spec from 2016, it mentions thread safety, and actually has it's own internal mutex to ensure it, so it seems surprising.

Your comment regarding chrome not committing the changes until after the stack completes, is this anecdotal or can you pass along more information?

Worst case, I think we can resolve with a StorageEvent to sync the locks, but that may make things much more complicated, so I'd like to avoid if possible.

@latentflip
Copy link
Author

latentflip commented Feb 15, 2018 via email

@chieffancypants
Copy link
Owner

Thanks, appreciate the heads up!

@latentflip latentflip closed this Jun 25, 2018
@timothycouchhsi
Copy link

I also discovered this issue with localstorage caching (or whatever is going on) after much investigation. Could we add a disclaimer on the readme or something that this mutex isn't guaranteed to work on Chrome? Not sure what other browsers as I haven't tested them.

Thanks!

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.

3 participants