Remove all Promises from Switch node

Promises are expensive and should not be used in the main
message handling path. The Switch node used them a lot if
the node references context - with a lot of duplicate code
to handle async and sync code paths.

This change modifies the code to use callbacks throughout
that are just as performant in either case.
This commit is contained in:
Nick O'Leary 2018-12-20 22:57:47 +00:00
parent 7f5d47f39d
commit 473a2ae275
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
1 changed files with 153 additions and 274 deletions

View File

@ -91,206 +91,117 @@ module.exports = function(RED) {
return _maxKeptCount; return _maxKeptCount;
} }
function getProperty(node,msg) { function getProperty(node,msg,done) {
if (node.useAsyncRules) { if (node.propertyType === 'jsonata') {
return new Promise((resolve,reject) => { RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => {
if (node.propertyType === 'jsonata') { if (err) {
RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { done(RED._("switch.errors.invalid-expr",{error:err.message}));
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else { } else {
RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { done(undefined,value);
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
} }
}); });
} else { } else {
if (node.propertyType === 'jsonata') { RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => {
try { if (err) {
return RED.util.evaluateJSONataExpression(node.property,msg); done(undefined,undefined);
} catch(err) { } else {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) done(undefined,value);
} }
} else { });
try {
return RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
} catch(err) {
return undefined;
}
}
} }
} }
function getV1(node,msg,rule,hasParts) { function getV1(node,msg,rule,hasParts,done) {
if (node.useAsyncRules) { if (rule.vt === 'prev') {
return new Promise( (resolve,reject) => { return done(undefined,node.previousValue);
if (rule.vt === 'prev') { } else if (rule.vt === 'jsonata') {
resolve(node.previousValue); var exp = rule.v;
} else if (rule.vt === 'jsonata') { if (rule.t === 'jsonata_exp') {
var exp = rule.v; if (hasParts) {
if (rule.t === 'jsonata_exp') { exp.assign("I", msg.parts.index);
if (hasParts) { exp.assign("N", msg.parts.count);
exp.assign("I", msg.parts.index); }
exp.assign("N", msg.parts.count); }
} RED.util.evaluateJSONataExpression(exp,msg,(err,value) => {
} if (err) {
RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { done(RED._("switch.errors.invalid-expr",{error:err.message}));
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else if (rule.vt === 'json') {
resolve("json"); // TODO: ?! invalid case
} else if (rule.vt === 'null') {
resolve("null");
} else { } else {
RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { done(undefined, value);
if (err) { }
resolve(undefined); });
} else { } else if (rule.vt === 'json') {
resolve(value); done(undefined,"json"); // TODO: ?! invalid case
} } else if (rule.vt === 'null') {
}); done(undefined,"null");
} else {
RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) {
if (err) {
done(undefined, undefined);
} else {
done(undefined, value);
}
});
}
}
function getV2(node,msg,rule,done) {
var v2 = rule.v2;
if (rule.v2t === 'prev') {
return done(undefined,node.previousValue);
} else if (rule.v2t === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => {
if (err) {
done(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
done(undefined,value);
}
});
} else if (typeof v2 !== 'undefined') {
RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) {
if (err) {
done(undefined,undefined);
} else {
done(undefined,value);
} }
}); });
} else { } else {
if (rule.vt === 'prev') { done(undefined,v2);
return node.previousValue;
} else if (rule.vt === 'jsonata') {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
if (hasParts) {
exp.assign("I", msg.parts.index);
exp.assign("N", msg.parts.count);
}
}
try {
return RED.util.evaluateJSONataExpression(exp,msg);
} catch(err) {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message}))
}
} else if (rule.vt === 'json') {
return "json"; // TODO: ?! invalid case
} else if (rule.vt === 'null') {
return "null";
} else {
try {
return RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg);
} catch(err) {
return undefined;
}
}
} }
} }
function getV2(node,msg,rule) { function applyRule(node, msg, property, state, done) {
if (node.useAsyncRules) { var rule = node.rules[state.currentRule];
return new Promise((resolve,reject) => { var v1,v2;
var v2 = rule.v2;
if (rule.v2t === 'prev') { getV1(node,msg,rule,state.hasParts, (err,value) => {
resolve(node.previousValue); if (err) {
} else if (rule.v2t === 'jsonata') { return done(err);
RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { }
if (err) { v1 = value;
reject(RED._("switch.errors.invalid-expr",{error:err.message})); getV2(node,msg,rule, (err,value) => {
} else { if (err) {
resolve(value); return done(err);
} }
}); v2 = value;
} else if (typeof v2 !== 'undefined') { if (rule.t == "else") {
RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { property = state.elseflag;
if (err) { state.elseflag = true;
resolve(undefined); }
} else { if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
resolve(value); state.onward.push(msg);
} state.elseflag = false;
}); if (node.checkall == "false") {
return done(undefined,false);
}
} else { } else {
resolve(v2); state.onward.push(null);
} }
}) done(undefined, state.currentRule < node.rules.length - 1);
} else { });
var v2 = rule.v2; });
if (rule.v2t === 'prev') {
return node.previousValue;
} else if (rule.v2t === 'jsonata') {
try {
return RED.util.evaluateJSONataExpression(rule.v2,msg);
} catch(err) {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message}))
}
} else if (typeof v2 !== 'undefined') {
try {
return RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg);
} catch(err) {
return undefined;
}
} else {
return v2;
}
}
} }
function applyRule(node, msg, property, state) { function applyRules(node, msg, property,state,done) {
if (node.useAsyncRules) {
return new Promise((resolve,reject) => {
var rule = node.rules[state.currentRule];
var v1,v2;
getV1(node,msg,rule,state.hasParts).then(value => {
v1 = value;
}).then(()=>getV2(node,msg,rule)).then(value => {
v2 = value;
}).then(() => {
if (rule.t == "else") {
property = state.elseflag;
state.elseflag = true;
}
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return resolve(false);
}
} else {
state.onward.push(null);
}
resolve(state.currentRule < node.rules.length - 1);
});
})
} else {
var rule = node.rules[state.currentRule];
var v1 = getV1(node,msg,rule,state.hasParts);
var v2 = getV2(node,msg,rule);
if (rule.t == "else") {
property = state.elseflag;
state.elseflag = true;
}
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return false;
}
} else {
state.onward.push(null);
}
return state.currentRule < node.rules.length - 1
}
}
function applyRules(node, msg, property,state) {
if (!state) { if (!state) {
state = { state = {
currentRule: 0, currentRule: 0,
@ -301,26 +212,18 @@ module.exports = function(RED) {
msg.parts.hasOwnProperty("index") msg.parts.hasOwnProperty("index")
} }
} }
if (node.useAsyncRules) { applyRule(node,msg,property,state,(err,hasMore) => {
return applyRule(node,msg,property,state).then(hasMore => { if (err) {
if (hasMore) { return done(err);
state.currentRule++; }
return applyRules(node,msg,property,state);
} else {
node.previousValue = property;
return state.onward;
}
});
} else {
var hasMore = applyRule(node,msg,property,state);
if (hasMore) { if (hasMore) {
state.currentRule++; state.currentRule++;
return applyRules(node,msg,property,state); applyRules(node,msg,property,state,done);
} else { } else {
node.previousValue = property; node.previousValue = property;
return state.onward; done(undefined,state.onward);
} }
} });
} }
@ -345,13 +248,6 @@ module.exports = function(RED) {
var valid = true; var valid = true;
var repair = n.repair; var repair = n.repair;
var needsCount = repair; var needsCount = repair;
this.useAsyncRules = (
this.propertyType === 'flow' ||
this.propertyType === 'global' || (
this.propertyType === 'jsonata' &&
/\$(flow|global)Context/.test(this.property)
)
);
for (var i=0; i<this.rules.length; i+=1) { for (var i=0; i<this.rules.length; i+=1) {
var rule = this.rules[i]; var rule = this.rules[i];
@ -363,13 +259,6 @@ module.exports = function(RED) {
rule.vt = 'str'; rule.vt = 'str';
} }
} }
this.useAsyncRules = this.useAsyncRules || (
rule.vt === 'flow' ||
rule.vt === 'global' || (
rule.vt === 'jsonata' &&
/\$(flow|global)Context/.test(rule.v)
)
);
if (rule.vt === 'num') { if (rule.vt === 'num') {
if (!isNaN(Number(rule.v))) { if (!isNaN(Number(rule.v))) {
rule.v = Number(rule.v); rule.v = Number(rule.v);
@ -382,9 +271,6 @@ module.exports = function(RED) {
valid = false; valid = false;
} }
} }
if (rule.vt === 'flow' || rule.vt === 'global' || rule.vt === 'jsonata') {
this.useAsyncRules = true;
}
if (typeof rule.v2 !== 'undefined') { if (typeof rule.v2 !== 'undefined') {
if (!rule.v2t) { if (!rule.v2t) {
if (!isNaN(Number(rule.v2))) { if (!isNaN(Number(rule.v2))) {
@ -393,13 +279,6 @@ module.exports = function(RED) {
rule.v2t = 'str'; rule.v2t = 'str';
} }
} }
this.useAsyncRules = this.useAsyncRules || (
rule.v2t === 'flow' ||
rule.v2t === 'global' || (
rule.v2t === 'jsonata' &&
/\$(flow|global)Context/.test(rule.v2)
)
);
if (rule.v2t === 'num') { if (rule.v2t === 'num') {
rule.v2 = Number(rule.v2); rule.v2 = Number(rule.v2);
} else if (rule.v2t === 'jsonata') { } else if (rule.v2t === 'jsonata') {
@ -444,26 +323,38 @@ module.exports = function(RED) {
return group; return group;
} }
function drainMessageGroup(msgs,count,done) {
function addMessageToPending(msg) { var msg = msgs.shift();
msg.parts.count = count;
processMessage(msg,false, err => {
if (err) {
done(err);
} else {
if (msgs.length === 0) {
done()
} else {
drainMessageGroup(msgs,count,done);
}
}
})
}
function addMessageToPending(msg,done) {
var parts = msg.parts; var parts = msg.parts;
// We've already checked the msg.parts has the require bits // We've already checked the msg.parts has the require bits
var group = addMessageToGroup(parts.id, msg, parts); var group = addMessageToGroup(parts.id, msg, parts);
var msgs = group.msgs; var msgs = group.msgs;
var count = group.count; var count = group.count;
if (count === msgs.length) { var msgsCount = msgs.length;
if (count === msgsCount) {
// We have a complete group - send the individual parts // We have a complete group - send the individual parts
return msgs.reduce((promise, msg) => { drainMessageGroup(msgs,count,err => {
return promise.then((result) => { pendingCount -= msgsCount;
msg.parts.count = count;
return processMessage(msg, false);
})
}, Promise.resolve()).then( () => {
pendingCount -= group.msgs.length;
delete pendingIn[parts.id]; delete pendingIn[parts.id];
}); done();
})
return;
} }
return Promise.resolve(); done();
} }
function sendGroup(onwards, port_count) { function sendGroup(onwards, port_count) {
@ -529,43 +420,33 @@ module.exports = function(RED) {
} }
} }
function processMessage(msg, checkParts, done) {
function processMessage(msg, checkParts) {
var hasParts = msg.hasOwnProperty("parts") && var hasParts = msg.hasOwnProperty("parts") &&
msg.parts.hasOwnProperty("id") && msg.parts.hasOwnProperty("id") &&
msg.parts.hasOwnProperty("index"); msg.parts.hasOwnProperty("index");
if (needsCount && checkParts && hasParts) { if (needsCount && checkParts && hasParts) {
return addMessageToPending(msg); addMessageToPending(msg,done);
}
if (node.useAsyncRules) {
return getProperty(node,msg)
.then(property => applyRules(node,msg,property))
.then(onward => {
if (!repair || !hasParts) {
node.send(onward);
}
else {
sendGroupMessages(onward, msg);
}
}).catch(err => {
node.warn(err);
});
} else { } else {
try { getProperty(node,msg,(err,property) => {
var property = getProperty(node,msg); if (err) {
var onward = applyRules(node,msg,property); node.warn(err);
if (!repair || !hasParts) { done();
node.send(onward);
} else { } else {
sendGroupMessages(onward, msg); applyRules(node,msg,property,undefined,(err,onward) => {
if (err) {
node.warn(err);
} else {
if (!repair || !hasParts) {
node.send(onward);
} else {
sendGroupMessages(onward, msg);
}
}
done();
});
} }
} catch(err) { });
node.warn(err);
}
} }
} }
@ -578,12 +459,13 @@ module.exports = function(RED) {
} }
var pendingMessages = []; var pendingMessages = [];
var activeMessagePromise = null; var handlingMessage = false;
var processMessageQueue = function(msg) { var processMessageQueue = function(msg) {
if (msg) { if (msg) {
// A new message has arrived - add it to the message queue // A new message has arrived - add it to the message queue
pendingMessages.push(msg); pendingMessages.push(msg);
if (activeMessagePromise !== null) { if (handlingMessage) {
// The node is currently processing a message, so do nothing // The node is currently processing a message, so do nothing
// more with this message // more with this message
return; return;
@ -592,27 +474,24 @@ module.exports = function(RED) {
if (pendingMessages.length === 0) { if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag // There are no more messages to process, clear the active flag
// and return // and return
activeMessagePromise = null; handlingMessage = false;
return; return;
} }
// There are more messages to process. Get the next message and // There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more // start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift(); var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg,true) handlingMessage = true;
.then(processMessageQueue) processMessage(nextMsg,true,err => {
.catch((err) => { if (err) {
node.error(err,nextMsg); node.error(err,nextMsg);
return processMessageQueue(); }
}); processMessageQueue()
});
} }
this.on('input', function(msg) { this.on('input', function(msg) {
if (node.useAsyncRules) { processMessageQueue(msg);
processMessageQueue(msg);
} else {
processMessage(msg,true);
}
}); });
this.on('close', function() { this.on('close', function() {