Cyril Duchon-Doris Cyril Duchon-Doris - 5 months ago 26
Node.js Question

Javascript callback function in blackboard pattern control flow

I have implemented a blackboard pattern in Javascript, my blackboard control iterates over knowledge sources / experts, and call their execAction().

for(let expert of this.blackboard.experts){
// Check execution condition
}
mostRelevantExpert.executeAction();


Now the problem is, those knowledge sources often need to call remote APIs or read files, and most of the libraries only provide callback APIs

class myExpert{
executeAction() {
myLibrary.call(params, (error, response) => { continueHere; })
}
}


Of course this is completely messing up the flow of my blackboard.

I am not sure whether the solution would be to reimplement the whole blackboard in an "asynchronous" fashion, or if there's a smarter way to go.

I've tried using libraries like deasync, but the problem is that I actually have a bug in
myLibrary.call(params, (error, response) => { bueHere; }
and I do not really understand now how to debug it. Since I am likely to have more problems like that in the future, was wondering what actions I should take.

Using node 6, ES6, and I don't like using callback programming style for what I'm doing here.


  • How should I go about the blackboard pattern in Javascript ?

  • How can I debug async code using
    node debug app.js



EDIT :

Here is my Blackboard Control code :

module.exports = class BlackboardControl{
constructor(blackboard){
this.blackboard = blackboard;
}

loop(){
console.log('ยค Blackboard Control');
console.log(' Starting Blackboard loop');

// Problem solved when there is a technicianAnswer, so the bot has something to say
while(!this.blackboard.problemSolved) {

// Select experts who can contribute to the problem
let candidates = [];
for(let expert of this.experts){
let eagerness = expert.canContribute();
if(eagerness){
candidates.push([eagerness,expert]);
}
}

if(candidates.length === 0) {
console.log('No expert can\'t do anything, returning');
return;
}

// Sort them by eagerness
candidates.sort(function(a,b) {
return a[0]-b[0];
});
for(let eagerExpert of candidates){
console.log('Next expert elected : ' + eagerExpert[1].constructor.name);
eagerExpert[1].execAction();

}
}
}
};

Answer

Here is an attempt based on my (incomplete) understanding of your problem. These are the premises I used:

  • you have Expert objects that provide an asynchronous function that does some kind of work via the executeAction() method.
  • you have a BlackboardControl object that pools these experts and is responsible for running them in sequence until one of them returns a successful result. This object is also holding some kind of state encapsulated in the blackboard property.

The first step to a promise-based solution is to make the executeAction() method return a promise instead of requiring a callback. Changing the call convention of an entire node-style library is easily done with the promisifyAll() utility that Bluebird provides:

// module MyExpert ---------------------------------------------------------
var Promise = require('bluebird');

// dummy library with a node-style async function, let's promisify it
var myLibrary = Promise.promisifyAll({
  someFunc: function (params, callback) {
    setTimeout(() => {
      if (Math.random() < 0.4) callback('someFunc failed');
      else callback(null, {inputParams: params});
    }, Math.random() * 1000 + 100);
  }
});

class MyExpert {
  executeAction(params) {
    return myLibrary.someFuncAsync(params);  // returns a promise!
  }
}

module.exports = MyExpert;

now, we need a BlackboardControl object that does two things: pull out the next free Expert object from the pool (nextAvailableExpert()) and solve a given problem by applying experts to it in sequence, until one of them succeeds or a maximum retry count is reached (solve()).

// module BlackboardControl ------------------------------------------------
var Promise = require('bluebird');
var MyExpert = require('./MyExpert');

class BlackboardControl {
  constructor(blackboard) {
    this.blackboard = blackboard;
    this.experts = [/* an array of experts */];
  }

  nextAvailableExpert() {
    return new MyExpert();

    // yours would look more like this
    return this.experts
      .map((x) => ({eagerness: x.canContribute(), expert: x}))
      .filter((ex) => ex.eagerness > 0)
      .sort((exA, exB) => exA.eagerness - exB.eagerness)
      .map((ex) => ex.expert)
      .pop();
  }

  solve(options) {
    var self = this;
    var expert = this.nextAvailableExpert();

    if (!expert) {
      return Promise.reject('no expert available');
    } else {
      console.info('Next expert elected : ' + expert.constructor.name);
    }

    options = options || {};
    options.attempt = +options.attempt || 0;
    options.maxAttempts = +options.maxAttempts || 10;

    return expert.executeAction(/* call parameters here */).catch(error => {
      options.attempt++;
      console.error("failed to solve in attempt " + options.attempt + ": " + error);
      if (options.attempt <= options.maxAttempts) return self.solve(options);
      return Promise.reject("gave up after " + options.maxAttempts + " attempts.");
    });
  }
}

module.exports = BlackboardControl;

The key line is this one:

if (options.attempt <= options.maxAttempts) return self.solve(options);

Promises chain. If you return a new promise from a promise callback (in this case from the catch() handler, since we want to start over when an expert fails) the overall result of the promise will be determined by the result of this new promise. In other words, the new promise will be executed. This is our iterative step.

This way returning a promise from solve() enables internal repetition by simply calling solve() again in the error handler - and it enables reacting externally via then() as shown in below example usage:

// usage -------------------------------------------------------------------
var BlackboardControl = require('./BlackboardControl');
var bbControl = new BlackboardControl({ /* blackboard object */ });

var result = bbControl.solve({
  maxAttempts: 10
}).then(response => {
  console.log("continueHere: ", response);
}).catch(reason => {
  console.error(reason);
});

which creates output like this (here dummy function happened to fail five times in a row):

Next expert elected : MyExpert
failed to solve in attempt 1: Error: someFunc failed
Next expert elected : MyExpert
failed to solve in attempt 2: Error: someFunc failed
Next expert elected : MyExpert
failed to solve in attempt 3: Error: someFunc failed
Next expert elected : MyExpert
failed to solve in attempt 4: Error: someFunc failed
Next expert elected : MyExpert
failed to solve in attempt 5: Error: someFunc failed
Next expert elected : MyExpert
continueHere:  { some: 'parameters' }

During expert runs control is returned to the main program. Due to the fact that now multiple experts can run at the same time on multiple problems we can't make a list of available experts up-front. We must make a fresh decision every time we need an expert, hence the nextAvailableExpert() function.

Comments