From 477e81256ecd8502d524326c834c464e55b20d54 Mon Sep 17 00:00:00 2001 From: Vasu Vanka Date: Tue, 10 Dec 2024 01:41:54 +0530 Subject: [PATCH 1/5] added default timeout to function node (#1) * added default timeout to function node * added unit test to support defaultFunctionTimeout --- .../nodes/core/function/10-function.js | 13 +++- packages/node_modules/node-red/settings.js | 25 ++++++- test/nodes/core/function/10-function_spec.js | 65 ++++++++++++++++++- 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 0120d8c92..b5fcb14d4 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -14,6 +14,8 @@ * limitations under the License. **/ +const { log } = require("console"); + module.exports = function(RED) { "use strict"; @@ -399,6 +401,8 @@ module.exports = function(RED) { if(node.timeout>0){ finOpt.timeout = node.timeout; finOpt.breakOnSigint = true; + } else if(RED.settings.defaultFunctionTimeout > 0){ + finOpt.timeout = RED.settings.defaultFunctionTimeout * 1000 } } var promise = Promise.resolve(); @@ -415,8 +419,15 @@ module.exports = function(RED) { var opts = {}; if (node.timeout>0){ opts = node.timeoutOptions; + } else if(RED.settings.defaultFunctionTimeout > 0){ + opts.timeout = RED.settings.defaultFunctionTimeout * 1000 + } + try { + node.script.runInContext(context,opts); + } catch (err) { + node.error(err); + return done(err); } - node.script.runInContext(context,opts); context.results.then(function(results) { sendResults(node,send,msg._msgid,results,false); if (handleNodeDoneCall) { diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index e8bb01228..fdacb7102 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -473,6 +473,7 @@ module.exports = { * - fileWorkingDirectory * - functionGlobalContext * - functionExternalModules + * - defaultFunctionTimeout * - functionTimeout * - nodeMessageBufferMaxLength * - ui (for use with Node-RED Dashboard) @@ -499,8 +500,30 @@ module.exports = { /** Allow the Function node to load additional npm modules directly */ functionExternalModules: true, + + /** + * Default function timeout (in seconds) for the Function node. + * A value of 0 indicates no timeout is applied, meaning the function can run indefinitely. + * + * The default function timeout is designed to prevent blocking code in function nodes, + * which could otherwise lead to a stalled or unresponsive main thread. For example, + * the following code would block the event loop indefinitely: + * + * `while(1) {}` + * + * By specifying a `defaultFunctionTimeout`, such scenarios can be mitigated, + * ensuring that long-running or infinite loops are terminated automatically after + * the specified timeout duration. + * + * Note: If both `defaultFunctionTimeout` and `functionTimeout` are defined in the + * settings file, `functionTimeout` takes precedence, providing a more granular + * control for individual function nodes. + */ + + defaultFunctionTimeout: 5, + /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ - functionTimeout: 0, + functionTimeout: 2, /** The following property can be used to set predefined values in Global Context. * This allows extra node modules to be made available with in Function node. diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 56c4ec976..3f7eedafd 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -1437,7 +1437,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function(evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(1); + logEvents.should.have.length(2); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1451,7 +1451,7 @@ describe('function node', function() { }); }); - it('check if default function timeout settings are recognized', function (done) { + it('check if function timeout settings are recognized', function (done) { RED.settings.functionTimeout = 0.01; var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; helper.load(functionNode, flow, function () { @@ -1463,7 +1463,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function (evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(1); + logEvents.should.have.length(2); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1479,6 +1479,65 @@ describe('function node', function() { }); }); + it('check if default function timeout settings are recognized', function (done) { + RED.settings.defaultFunctionTimeout = 0.01; + var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(2); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.defaultFunctionTimeout, 0.01); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.defaultFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + + it('check if functionTimeout has higher precedence over default function timeout setting', function (done) { + RED.settings.defaultFunctionTimeout = 0.02; + RED.settings.functionTimeout = 0.01; + var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(2); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.functionTimeout, 0.01); + should.equal(RED.settings.defaultFunctionTimeout, 0.02); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.functionTimeout; + delete RED.settings.defaultFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + describe("finalize function", function() { it('should execute', function(done) { From d0d838bc909638809da18e7b14421f0cb50c4b06 Mon Sep 17 00:00:00 2001 From: Vasu Vanka Date: Wed, 11 Dec 2024 11:05:20 +0530 Subject: [PATCH 2/5] default function timeout PR comments (#2) set values to default and removed unused import --- packages/node_modules/node-red/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index fdacb7102..f45b2c240 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -520,7 +520,7 @@ module.exports = { * control for individual function nodes. */ - defaultFunctionTimeout: 5, + defaultFunctionTimeout: 0, /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ functionTimeout: 2, From 6a127e98cc04630cf0c40de98b945dbf713140cd Mon Sep 17 00:00:00 2001 From: Vasu Vanka Date: Wed, 11 Dec 2024 11:08:00 +0530 Subject: [PATCH 3/5] Update 10-function.js --- .../node_modules/@node-red/nodes/core/function/10-function.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index b5fcb14d4..6205af8f1 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -14,8 +14,6 @@ * limitations under the License. **/ -const { log } = require("console"); - module.exports = function(RED) { "use strict"; From c1fbff1e1894ab27cc4daea98b47a9898f272275 Mon Sep 17 00:00:00 2001 From: Vasu Vanka Date: Wed, 11 Dec 2024 11:09:33 +0530 Subject: [PATCH 4/5] Update settings.js --- packages/node_modules/node-red/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index f45b2c240..2c589a62e 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -523,7 +523,7 @@ module.exports = { defaultFunctionTimeout: 0, /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ - functionTimeout: 2, + functionTimeout: 0, /** The following property can be used to set predefined values in Global Context. * This allows extra node modules to be made available with in Function node. From 9e79fc8a09563af552feedff312b1a4ff0f062e7 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 6 Jun 2025 11:30:34 +0100 Subject: [PATCH 5/5] Apply suggestions from code review --- .../nodes/core/function/10-function.js | 9 +++--- packages/node_modules/node-red/settings.js | 30 +++++++------------ test/nodes/core/function/10-function_spec.js | 20 ++++++------- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 6205af8f1..e2383c114 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -399,8 +399,8 @@ module.exports = function(RED) { if(node.timeout>0){ finOpt.timeout = node.timeout; finOpt.breakOnSigint = true; - } else if(RED.settings.defaultFunctionTimeout > 0){ - finOpt.timeout = RED.settings.defaultFunctionTimeout * 1000 + } else if (RED.settings.globalFunctionTimeout > 0){ + finOpt.timeout = RED.settings.globalFunctionTimeout * 1000 } } var promise = Promise.resolve(); @@ -417,13 +417,12 @@ module.exports = function(RED) { var opts = {}; if (node.timeout>0){ opts = node.timeoutOptions; - } else if(RED.settings.defaultFunctionTimeout > 0){ - opts.timeout = RED.settings.defaultFunctionTimeout * 1000 + } else if (RED.settings. globalFunctionTimeout > 0){ + opts.timeout = RED.settings. globalFunctionTimeout * 1000 } try { node.script.runInContext(context,opts); } catch (err) { - node.error(err); return done(err); } context.results.then(function(results) { diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 2c589a62e..d6b3d993c 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -473,7 +473,7 @@ module.exports = { * - fileWorkingDirectory * - functionGlobalContext * - functionExternalModules - * - defaultFunctionTimeout + * - globalFunctionTimeout * - functionTimeout * - nodeMessageBufferMaxLength * - ui (for use with Node-RED Dashboard) @@ -502,27 +502,17 @@ module.exports = { /** - * Default function timeout (in seconds) for the Function node. - * A value of 0 indicates no timeout is applied, meaning the function can run indefinitely. - * - * The default function timeout is designed to prevent blocking code in function nodes, - * which could otherwise lead to a stalled or unresponsive main thread. For example, - * the following code would block the event loop indefinitely: - * - * `while(1) {}` - * - * By specifying a `defaultFunctionTimeout`, such scenarios can be mitigated, - * ensuring that long-running or infinite loops are terminated automatically after - * the specified timeout duration. - * - * Note: If both `defaultFunctionTimeout` and `functionTimeout` are defined in the - * settings file, `functionTimeout` takes precedence, providing a more granular - * control for individual function nodes. + * The default timeout (in seconds) for all Function nodes. + * Individual nodes can set their own timeout value within their configuration. */ + globalFunctionTimeout: 0, - defaultFunctionTimeout: 0, - - /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ + /** + * Default timeout, in seconds, for the Function node. 0 means no timeout is applied + * This value is applied when the node is first added to the workspace - any changes + * must then be made with the individual node configurations. + * To set a global timeout value, use `globalFunctionTimeout` + */ functionTimeout: 0, /** The following property can be used to set predefined values in Global Context. diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 3f7eedafd..6a04547f4 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -1437,7 +1437,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function(evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(2); + logEvents.should.have.length(1); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1463,7 +1463,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function (evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(2); + logEvents.should.have.length(1); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1480,7 +1480,7 @@ describe('function node', function() { }); it('check if default function timeout settings are recognized', function (done) { - RED.settings.defaultFunctionTimeout = 0.01; + RED.settings.globalFunctionTimeout = 0.01; var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; helper.load(functionNode, flow, function () { var n1 = helper.getNode("n1"); @@ -1491,14 +1491,14 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function (evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(2); + logEvents.should.have.length(1); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); msg.should.have.property('type', 'function'); - should.equal(RED.settings.defaultFunctionTimeout, 0.01); + should.equal(RED.settings.globalFunctionTimeout, 0.01); should.equal(msg.msg.message, 'Script execution timed out after 10ms'); - delete RED.settings.defaultFunctionTimeout; + delete RED.settings.globalFunctionTimeout; done(); } catch (err) { done(err); @@ -1508,7 +1508,7 @@ describe('function node', function() { }); it('check if functionTimeout has higher precedence over default function timeout setting', function (done) { - RED.settings.defaultFunctionTimeout = 0.02; + RED.settings.globalFunctionTimeout = 0.02; RED.settings.functionTimeout = 0.01; var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; helper.load(functionNode, flow, function () { @@ -1520,16 +1520,16 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function (evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(2); + logEvents.should.have.length(1); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); msg.should.have.property('type', 'function'); should.equal(RED.settings.functionTimeout, 0.01); - should.equal(RED.settings.defaultFunctionTimeout, 0.02); + should.equal(RED.settings.globalFunctionTimeout, 0.02); should.equal(msg.msg.message, 'Script execution timed out after 10ms'); delete RED.settings.functionTimeout; - delete RED.settings.defaultFunctionTimeout; + delete RED.settings.globalFunctionTimeout; done(); } catch (err) { done(err);