Monday, March 21, 2016

Merging Multiple AJAX threads using a "Joining Mutex"

I had previously posted this article with a snippet of example code. That blog entry was accidently deleted so I am reposting an update version of it now.

What is a "Joining Mutex" and why would you need it?

var AfterAllThreadsExecute = function() { alert("Hello World");}

for(var i=0; i < records.length; i++) {
        // dispatchAjaxCall(ThreadMutexHandle, recordId)
 dispatchAjaxCall(JoiningMutex.contextJoinCreate('TestContext',AfterAllThreadsExecute, records[i].ID);
}


within the dispatchAjaxCall(myMutexHandle) {

 // do AJAX stuff here ...

 // now signal to the Mutex (via our reference handle) that this thread has finish running
 myMutexHandle.ThreadFinished();
}
A listing of the initial code is below. The latest code is used within the i2b2 web client in the i2b2/hive/hive_helpers.js file and can be found at https://github.com/i2b2/i2b2-webclient/blob/master/js-i2b2/hive/hive_helpers.js
// created this object to allow the joining of forked execution paths (Waiting for Multiple AJAX calls)
JoiningMutex = { 
 _contexts: {},
 _contextGenID: 0,
 _createContextProxy: function(contextRef) {
  // create a proxy object (via closure) to encapsulate data 
  // and route actions to the JoinMutex singleton
  var cl_JoinMutextRef = contextRef;
  function JoiningMutexContextProxy() {
   this._JoiningMutexContext = cl_JoinMutextRef;
   this._alreadyRun = false;
   this.name = function() { return this._JoiningMutexContext.name; };
   this.openThreads = function() { return this._JoiningMutexContext.openThreads; };
   this.executeOnce = function() { return this._JoiningMutexContext.executeOnce; };
   this.executionCount = function() { return this._JoiningMutexContext.executionCount; };
   this.isActive = function() { return this._JoiningMutexContext.active; };
   this.ThreadFinished = function() {
    if (!this._JoiningMutexContext.active) {
     return {error: true, errorObj: undefined, errorMsg: 'JoiningMutexProxy.ThreadFinished() failed because the giving context is no longer active'};
    }
    if (this._JoiningMutexContext._alreadyRun) {
     return {error: true, errorObj: undefined, errorMsg: 'JoiningMutexProxy.ThreadFinished() failed because the MutexProxy has already been run'};
    }
    if (this._JoiningMutexContext.openThreads > 0) {
     this._JoiningMutexContext.openThreads--;
     this._alreadyRun = true;
     if (this._JoiningMutexContext.openThreads == 0) {
      // all threads finished
      if (this._JoiningMutexContext.executeOnce) {
       // this is going to be our only run of the callback function
       this._JoiningMutexContext.active = false;
      }
      this._JoiningMutexContext.executionCount++;
      this._JoiningMutexContext.callbackFinished();
      return true;
     } else {
      // everything is OK but there are still outstanding threads to finish
      return false;
     }
    } else {
     return {error: true, errorObj: undefined, errorMsg: 'JoiningMutexProxy.ThreadFinished() failed because there are no outstanding thread executions'};
    }
   };
  }
  return new JoiningMutexContextProxy;
 },
 contextCreate: function(sContextName, fZeroRunFunction, bSingleRun) {
  // make sure context is new
  var validName = sContextName;
  try {
   if (!validName) {
    this._contextGenID++;
    validName = "AUTOGEN-"+this._contextGenID;
   }
   if (this._contexts[validName]) {
    return {error: true, errorObj: undefined, errorMsg: 'JoiningMutex.contextCreate() failed because the giving context name already exists'};
   }
   // verify that the name can be used as an object identifier (with throw an error if invalid)
   this._contexts[validName] = true;
   delete this._contexts[validName];
  } catch(e) {
   return {error: true, errorObj: e, errorMsg: 'an error occurred within JoiningMutex.contextCreate()'};
  }
  // create new context object
  function JoiningMutexContext(inName, inFinishFunction, inSingleExecution) {
   this.name = inName;
   this.callbackFinished = inFinishFunction;
   this.openThreads = 0;
   this.executeOnce = inSingleExecution;
   this.executionCount = 0;
   this.active = true;
  }
  var bSingleRun = Boolean.parseTo(bSingleRun);
  this._contexts[validName] = new JoiningMutexContext(validName, fZeroRunFunction, bSingleRun);
  // add ourselves to the thread count
  var cl_JoinMutextRef = this._contexts[validName];
  cl_JoinMutextRef.openThreads++;
  return this._createContextProxy(cl_JoinMutextRef);
 },
 contextJoin: function(sContextName) {
  // make sure context already exists
  var validName = sContextName;
  if (!this._contexts[validName]) {
   return {error: true, errorObj: undefined, errorMsg: 'JoiningMutex.contextCreate() failed because the context name does not exist'};
  }
  var cl_JoinMutextRef = this._contexts[validName];
  // Add this thread to the count
  cl_JoinMutextRef.openThreads++;
  return this._createContextProxy(cl_JoinMutextRef);
 },
 contextJoinCreate: function(sContextName, fZeroRunFunction, bSingleRun) {
  // Join context or create it if it exists
  var ctx = false;
  if (!sContextName || !this._contexts[sContextName]) {
   ctx = this.contextCreate.call(this, sContextName, fZeroRunFunction, bSingleRun);
  } else {
   ctx = this.contextJoin.call(this, sContextName);
  }
  return ctx;
 },
 contextDestroy: function(sContextName) {
  if (!sContextName || !this._contexts[sContextName]) {
   return false;
  } else {
   // Garbage collection will not execute until all 
   // the JoiningMutexContextProxy are deleted so 
   // invalidate the context as well as delete it!
   this._contexts[sContextName].active = false;
   this._contexts[sContextName].callbackFinished = function() { return null; };
   delete this._contexts[sContextName];
   return true;
  }
 }
};