1

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);
Community
  • 1
  • 1
Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • `postMessage` would be another way to have the tabs “communicate” with each other. Whether that will work for what you’re planning to do, might depend on the specifics what you need to “execute”. – CBroe Oct 23 '15 at 23:24
  • @CBroe If I understand correctly, `postMessage` requires references to the other windows, correct? Unfortunately I don't have these (see [here](http://stackoverflow.com/questions/17356681/html5-postmessage-without-window-reference) and point 4 in the requirements list) – Markus A. Oct 23 '15 at 23:25
  • Yeah, my bad. Still, could you expand a little bit on what you actually need to execute every few seconds, and why it matters so much that it only happens from one particular tab? – CBroe Oct 23 '15 at 23:30
  • @CBroe I have a couple different use-cases actually, but the one I'm currently working on is: I have multiple tabs/windows continuously generating data (a couple events per seconds) that needs to be sent to the server. To save myself and the users bandwidth, I would like to collect this data, save it to LocalStorage, and batch-upload it only every so often, rather than sending one server-request per event. – Markus A. Oct 23 '15 at 23:37
  • Does it matter that the data be send in “exact” intervals, or would plus/minus a few seconds not be an issue? Would it matter if data was send twice occasionally? – CBroe Oct 23 '15 at 23:39
  • Have you tried something like having one tab mark itself as the “active” one for this process, and store that information in localStorage (f.e. via a random tab name generated on load), and then have all other tabs look whether there is an active one – if so, they let it take over the task of sending the data, and if not the first one of the others makes itself the new active one. Combined with a timestamp that the active tab sets to mark when it last performed the task, the others could determine if the active one was still there, or maybe closed by the user. – CBroe Oct 23 '15 at 23:43
  • @CBroe The intervals don't need to be exact. It's even ok if an update is skipped on occasion. – Markus A. Oct 23 '15 at 23:43
  • @CBroe I have been trying to come up with a way to do that, yes. But it's not quite as trivial as I initially thought. You still basically need to implement some sort of mutual exclusion algorithm between the tabs/windows to guarantee things to work reliably. Unfortunately it is **not** acceptable under any circumstances that two processes enter the critical section at the same time as that will most likely mess up the data in LocalStorage and I will lose events. – Markus A. Oct 23 '15 at 23:45
  • @CBroe If several tabs/windows load at the exact same time (all are waiting to load and the network connection just restored, or the user re-opened a previous browser session with all tabs), it's not that easy to reliably decide which tab will be in charge. – Markus A. Oct 23 '15 at 23:48
  • Some kind of “locking”/semaphore on the localStorage then maybe? Have a tab that wants to write to/manipulate data in the LS acquire a lock first, and if there already is a lock in place, then that means it has to wait and retry after a short time (setTimeout) …? – CBroe Oct 23 '15 at 23:49
  • @CBroe Yep. That's exactly what the above algorithm is trying to do. :) But given the delay in the data-propagation in LocalStorage, I don't know of any semaphore or locking-algorithm that can handle the situation reliably. – Markus A. Oct 23 '15 at 23:51

0 Answers0