Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistency when writing synchronous to localStorage from multiple tabs

tl;dr I noticed inconsistent behaviour between browsers when writing to localStorage at the exact same time.

Requirement: Even when multiple tabs are open, a specific action (POST-request to refresh OAuth session) should be executed only once. Which tab executes the action does not matter. The point in time to do the refresh derives from the expiration time of the session and is the exact same in all tabs.

Approch: All tabs generate a random number, store it and write to localStorage. They then read the localStorage and if both are the same, then the tab is allowed to execute the action.

let tab = Math.random();
localStorage.setItem('tab',tab);
if(JSON.parse(localStorage.getItem('tab')) === tab) {
    console.log('aquired lock');
} else {
    console.log('did not aquire lock');
}

JSFiddle - In order to test the behaviour you need to open the fiddle in two tabs and then press Run in both. The timeout is calculated to execute at the next full 10-seconds. (Second 0, 10, 20, 30, 40, 50)

Expectation: Tabs A and B set localStorage['tab'] to a random value, on retriving the value only one tab retrieves the same value as it randomly generated and therefore is allowed to execute the action.

Result: Both A and B still retrieve their own generated value.

I added some timeout the let the memory-dust settle:

let tab = Math.random();
localStorage.setItem('tab',tab);
setTimeout(function(){
    if(JSON.parse(localStorage.getItem('tab')) === tab) {
        console.log('aquired lock');
    } else {
        console.log('did not aquire lock');
    }
}, 1000);

JSFiddle

Result (Firefox): Tab A retrieves value tab B generated, and vice versa. So no tab is allowed to execute the action.

This is where I got a bit spooked. I checked for console-timestamps, which where exactly the same, and the localStorage in the dev-tools, which showed different values in the different tabs. (Even reloading the tab did show different values in different tabs.)

If writing the value later on (e.g. via the console) all tabs update the value accordingly.

Result (Chrome, Edge): Only one tab logs aquired lock as expected.


Is there any explaination why Firefox can have different values in localStorage per tab?


I already solved the problem by subscribing to the StorageEvent. The tab with the smallest random-number gets to executed the action.

Used browsers:

  • Firefox 68.0 and 60.8.0esr (both 64bit)
  • Chrome 75.0.3770.142 (64bit)
  • Edge 44.18362.1.0
like image 949
SpazzMarticus Avatar asked Jul 18 '19 07:07

SpazzMarticus


Video Answer


1 Answers

This is a fairly standard situation when dealing with shared memory. Each thread accessing shared memory is allowed to keep its own copy (a "cache") for performance reasons until/unless some synchronization occurs, at which point the local copy must be reconciled with the shared copy.

The old storage specification talked about acquiring a storage mutex on every storage operation:

Whenever the properties of a localStorage attribute's Storage object are to be examined, returned, set, or deleted, whether as part of a direct property access, when checking for the presence of a property, during property enumeration, when determining the number of properties present, or as part of the execution of any of the methods or attributes defined on the Storage interface, the user agent must first obtain the storage mutex.

But that specification has been subsumed into the WHAT-WG "HTML" specification (which is about a lot more than HTML) in §11 ("Web Storage") and the requirement that every operation must acquire a storage mutex has been dropped. (I don't know why, but I would guess for performance reasons.) The current specification says:

Warning: The localStorage attribute provides access to shared state. This specification does not define the interaction with other browsing contexts in a multiprocess user agent, and authors are encouraged to assume that there is no locking mechanism.

The specification also doesn't discuss synchronization of storage across browsing contexts. That means implementations are free to optimize.

Looking into it with a modified version of your script, it looks like Firefox optimizes by having a local copy of local storage for each browsing context (tab) which it appears to update based on the storage event from other contexts (tabs). But if both tabs set the value (generating a storage event for the other tab) before the storage event from the other tab is processed, they both get and process the storage event from the other, updating with that value (the other tab's value), causing the behavior you describe.

Side note: The operation writing to persistent storage (what a third tab would see if you opened it after doing all this) also appears to be asynchronous, and the two tabs are in a race to see which one writes last (a race that is not always won by the last one writing to its local copy!).

This is effectively a large-scale version of what happens with shared memory between threads when there's only loose synchronization between the threads and no locking semantics, which the spec no longer requires.

Chrome would appear to be doing locking or similar.

like image 155
T.J. Crowder Avatar answered Oct 19 '22 12:10

T.J. Crowder