Cross-window synchronization (critical sections) in the browser

190 views Asked by At

I'm trying to achieve the following in a web page:

  • Users can open multiple tabs/windows of the page.
  • Every few seconds, I need exactly one of those tabs/windows to execute a specific section of code (critical region).
  • I don't care which one of the tabs/windows executes the code, i.e. no need to worry about the fairness or starvation properties of the solution.
  • Since the user opened the tabs/windows him/herself, the different instances of the page have no knowledge about or direct references to each other (i.e. no window.parent, etc.)
  • I don't want to require Flash or Silverlight or other plugins and everything needs to run client-side, so the ways in which the tabs/windows can communicate are very limited (LocalStorage is the only one I found so far, but there might be others).
  • Any of the tabs/windows can crash or be closed or refreshed at any time and more tabs/windows can be opened at any time also, and the remaining windows must "react" such that I still get exactly one execution of the critical region every few seconds.
  • This needs to run reliably in as many browsers as possible, including mobile (caniuse-rating of over %90).

My first attempt at a solution was to use a simple mutual exclusion algorithm that uses LocalStorage as the shared memory. For various reasons, I chose the mutual exclusion algorithm by Burns and Lynch from their paper "Mutual Exclusion Using Indivisible Reads and Writes" (page 4 (836)).

I built a jsfiddle (see code below) to try the idea out and it works beautifully in Firefox. If you'd like to try it, open the link to the fiddle in several (up to 20) windows of Firefox and watch exactly one of them blink orange every second. If you see more than one blink at the same time, let me know! :) (Note: the way I assign the IDs in the fiddle is a little cheesy (simply looping over 0..19) and things will only work if every window was assigned a different ID. If two windows show the same ID, simply reload one.).

Unfortunately, in Chrome and especially in Internet Explorer things don't work as planned (multiple windows blink). I think this is due to a delay in the propagation of the data I write to LocalStorage from one tab/window to the other (see my question about this here).

So, basically, I need to find either a different mutex algorithm that can handle delayed data (sounds difficult/impossible) or I need to find an entirely different approach. Maybe StorageEvents can help? Or maybe there is a different mechanism that doesn't use LocalStorage?

For completeness, here is the code of the fiddle:

// Global constants
var LOCK_TIMEOUT =  300; // Locks time out after 300ms
var INTERVAL     = 1000; // Critical section should run every second



//==================================================================================
// Assign process ID

var myID;
id = window.localStorage.getItem("id");

if (id==null) id = 0;
id = Number(id);
myID = id;
id = (id+1) % 20;
window.localStorage.setItem("id", id);

document.documentElement.innerHTML = "ID: "+myID;



//==================================================================================
// Method to indicate critical section

var lastBlink = 0;
function blink() {
    col = Math.round(Math.min((new Date().getTime() - lastBlink)*2/3, 255));
    document.body.style.backgroundColor = "rgb(255, "+((col >> 1)+128)+", "+col+")";
}



//==================================================================================
// Helper methods to implement expiring flags

function flagUp() {
    window.localStorage.setItem("F"+myID, new Date().getTime());
}

function flagDown() {
    window.localStorage.setItem("F"+myID, 0);
}

// Try to refresh flag timeout and return whether we're sure that it never expired
function refreshFlag() {
    content = window.localStorage.getItem("F"+myID);
    if (content==null) return false;
    content = Number(content);
    if ((content==NaN) || (Math.abs(new Date().getTime() - content)>=timeout))
        return false;
    window.localStorage.setItem("F"+myID, new Date().getTime());
    return Math.abs(new Date().getTime() - content) < timeout;
}    

function setFlag(key) {
    window.localStorage.setItem(key, new Date().getTime());
}

function checkFlag(key, timeout) {
    content = window.localStorage.getItem(key);
    if (content==null) return false;
    content = Number(content);
    if (content==NaN) return false;
    return Math.abs(new Date().getTime() - content) < timeout;
}



//==================================================================================
// Burns-Lynch mutual exclusion algorithm

var atLine7 = false;

function enterCriticalRegion() {

    // Refresh flag timeout and restart algorithm if flag may have expired
    if (atLine7) atLine7 &= refreshFlag();

    // Check if run is due
    if (checkFlag("LastRun", INTERVAL)) return false;

    if (!atLine7) {
        // 3: F[i] down
        flagDown();

        // 4: for j:=1 to i-1 do if F[j] = up goto 3
        for (j=0; j<myID; j++)
            if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

        // 5: F[i] up
        flagUp();

        // 6: for j:=1 to i-1 do if F[j] = up goto 3
        for (j=0; j<myID; j++)
            if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

        atLine7 = true;
    }

    // 7: for j:=i+1 to N do if F[j] = up goto 7
    for (j=myID+1; j<20; j++)
        if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

    // Check again if run is due
    return !checkFlag("LastRun", INTERVAL);
}

function leaveCriticalRegion() {
    // Remember time of last succesful run
    setFlag("LastRun");

    // Release lock on critical region
    atLine7 = false;
    window.localStorage.setItem("F"+myID, 0);
}



//==================================================================================
// Keep trying to enter critical region and blink on success

function run() {
    if (enterCriticalRegion()) {
        lastBlink = new Date().getTime();
        leaveCriticalRegion();
    }
}

// Go!
window.setInterval(run,   10);
window.setInterval(blink, 10);
0

There are 0 answers