Hi all,
I have a custom node which uses a number of websocket connections and streams.
when I use the Full Deploy button , my node and all other nodes restarts as intended. However, when I use Deploy Modified nodes only , I noticed a strange behaviour where I am getting data streams from the old node still.
When I debugged this I noted that even after the Deploy using Modified Nodes only option, the old config values seem to still linger along side the new config. It is as if I have two versions of the node running at the same time.
I would have expected that the modified node completely restarts similar to the Full Deploy option, that is references are dropped and node is reset.
We would probably have to look at the code to see what is happening. The first thing to ask though is whether you understand the different parts of a node's runtime and which bits get run when? It isn't necessarily all that obvious until you've thought it through. Kept catching me out on more complex nodes which is why I took the time to de-structure my nodes to make it more obvious to me and to simplify parts of the structure.
@TotallyInformation what would be an example of those runtime internals that I need to consider when I use the Deploy Modified nodes only button ? In other words, what is actually happening differently than a Full Deploy ? I dont have knowledge of that
There is code outside the module.exports which is run when the node is first imported by Node-RED. Any variables and function stay in memory but are not accessible outside your module.
The exported code is also run when the node is imported and it is passed the RED object. It calls the registration function.
The registration function is called each time an actual instance of your node is added to a flow.
It actually creates the node instance using RED.nodes.createNode and only after that call do you get a meaningful this object that represents the instance of the node.
The config object that contains the deployed variables from the Editor is passed to this function and you will want to apply the config properties to this so that they are on each instance.
This function also contains the functions for handling incoming messages to your node instance and, if needed, a close function for tidying up on re-deployment or removal of the instance.
The input event handler is called each time a message is "sent" to your node instance. As the function is defined within (3), it inherits 3's this context.
As you can see, there is considerable depth of embedding of functions within each other and it can be hard to understand each context.
That is why I de-construct my nodes to make the divisions easier to parse. You can find an example in my GitHub node-red-experimental-nodes repo, check out the new-node-template folder. While that structure is certainly more complex than needed for really simple nodes, as your node increases in complexity, it helps keep logical separation between the various moving parts.
I usually never put any code outside of my module.exports scope because I can't think of a use case for this. what would you possibly use this scope for if any at all ? or do you mean the global scope of module.exports function ?
not sure I am following you here.. the exported function, that is, the module.exports function runs on initialisation of the runtime only. It does not run when the node is "added to a flow" , that is just dragged and added to the flow or deployed. It runs with the passed RED which is the runtime node-red object giving access to different utils including nodes.registerType used to register the type. This runs once per node type.
the registration function, that is, the function designated for use as runtime for the instance is called every time the instance is "deployed" to the runtime with the config, therefore this should be re-initialised every time. the previously deployed instance in runtime should have been discarded at that point, ready to create an instance of the node. then RED.nodes.createNode simply applies the config to that instance.
I take the difference between the Full Deploy, and Modified Only to be nothing but either calling every registration function for all nodes or calling only the ones modified.
I would need to put a sample and replicate the behavior but I will see how I go with the new version and if this is a possible issue to report.
Not sure I maybe missing something but that's OK and thank you as this promoted me to dig bit deeper in the runtime code..
Things put there are part of your module but are not reachable from the rest of node-red's code or modules. It is a good place to put things that are common to all instances of your nodes. The thing to note is that they are executed only when Node-RED starts up (strictly when node-red loads your module).
In uibuilder for example, that is where I load dependent modules and where I create a common variable object some elements of which are set as the uibuilder node initialises. For example all of the static variables, a copy of the RED object and so on. I also set up the logging function there.
//#region ------ uibuilder module-level globals ------ //
/** @type {uibConfig} */
const uib = {
me: fs.readJSONSync(path.join( __dirname, '..', 'package.json' )),
moduleName: 'uibuilder',
nodeRoot: '',
deployments: {},
instances: {},
masterTemplateFolder: path.join( __dirname, '..', 'templates' ),
masterStaticFeFolder: path.join( __dirname, '..', 'front-end' ),
rootFolder: null,
configFolder: null,
configFolderName: '.config',
commonFolder: null,
commonFolderName: 'common',
sioUseMwName: 'sioUse.js',
sioMsgOutMwName: 'sioMsgOut.js',
ioChannels: { control: 'uiBuilderControl', client: 'uiBuilderClient', server: 'uiBuilder' },
nodeVersion: process.version.replace('v', '').split('.'),
staticOpts: {}, // { maxAge: 31536000, immutable: true, },
deleteOnDelete: {},
customServer: { // set correctly in libs/web.js:_webSetup()
port: undefined,
type: 'http',
host: undefined,
hostName: undefined,
isCustom: false,
serverOptions: {},
},
reDeployNeeded: '4.1.2',
degitEmitter: undefined,
RED: null,
instanceApiAllowed: false,
}
/** Current module version (taken from package.json) @constant {string} uib.version */
uib.version = uib.me.version
/** Dummy logging
* @type {Object<string, Function>} */
const dummyLog = {
fatal: function() {}, // fatal - only those errors which make the application unusable should be recorded
error: function() {}, // error - record errors which are deemed fatal for a particular request + fatal errors
warn: function() {}, // warn - record problems which are non fatal + errors + fatal errors
info: function() {}, // info - record information about the general running of the application + warn + error + fatal errors
debug: function() {}, // debug - record information which is more verbose than info + info + warn + error + fatal errors
trace: function() {}, // trace - record very detailed logging + debug + info + warn + error + fatal errors
}
let log = dummyLog // reset to RED.log or anything else you fancy at any point
// Placeholder - set in export
let userDir = ''
//#endregion ----- uibuilder module-level globals ----- //
I also set up a bunch of functions there which are then referenced from within the function that actually gets exported.
I didn't explain very well. If you look at the above. The function called nodeInstance is run for each instance of your node that is added to a flow. It instigates a this object and one of the early things you do is run RED.nodes.createNode(this, config) which instantiates the this as an node instance object. You then assign the settings from the Editor which are passed via the config argument to this to get the full node instance.