With the change to asynchronous message passing in Node-RED 1.0, we’re also changing how some messages are cloned between nodes. The behaviour in this area wasn’t always clear and could lead to unexpected results to end users who weren’t familiar with some of the principles of JavaScript object handling.
This post explains what cloning messages is all about, why it’s necessary and what is changing in 1.0.
Pass-by-reference
Before we get into the details of cloning messages, we once again take a detour into how JavaScript works.
Let’s consider the following code:
> let a = { payload: "hello" };
> let b = a;
> b.payload = "goodbye";
> console.log(a)
{ payload: 'goodbye' }
We create a new object and assign it to the variable a
. We then assign variable
b
to the value of a
. Next we change the value of b.payload
. Finally we print
the original variable a
.
As if by magic, the change we made to variable b
has been made to variable a
as well. This is because we did not make a copy of the object - we created a new
reference to the same object in memory.
This is known as pass-by-reference and can cause unexpected results if you aren’t prepared for it.
Cloning messages
Within a Node-RED flow, when a node receives a message (which is a JavaScript Object), it is free to modify the message however it wants and then pass it on to any nodes it is connected to.
When Node A sends a message it generates two ‘send events’ - one for Node B and one for Node C. If we simply passed the message to Node B then to Node C, any modifications to the message made by Node B would be visible to Node C.
This is where the need to clone messages comes in. Node-RED automatically clones messages before they are passed on in order to prevent this type of cross-branch modification.
But it isn’t quite as simple as that. Cloning messages can be expensive to do. For flows that are single row of nodes with no branching, there is in general no need to do any cloning of messages.
So the code tries to optimise when it will clone a message or not. The algorithm is:
When
node.send()
is called it generates a list of send events. The first send event uses the message object given as-is. All of the remaining send events clone their message before passing it on.
Essentially that means, for a node wired to one other node, a call to
node.send(msg)
will not clone that message because there is no need to.
But this algorithm has its limits. In particular, we cannot handle the case
where a Function node calls node.send()
multiple times with the same
message object.
For example, consider the following code from a Function node:
msg.topic = "A";
msg.payload = "1";
node.send(msg);
msg.topic = "B";
msg.payload = "2";
node.send(msg);
Each individual call to node.send()
will generate one send event, so the message
does not get cloned.
For some flows, this was not a problem prior to Node-RED 1.0. If the latter nodes
in the flow were entirely synchronous, the first call to node.send()
would pass
the whole way down the flow and complete before the execution would return to the
function and modify the message for the second call.
But with Node-RED 1.0 introducing fully asynchronous message passing, this pattern
of code would potentially be unsafe to use. The message would get modified before
the send event from the first node.send()
call is delivered.
This can cause quite subtle issues that are hard spot. There’s no loud bang to alert
the user to a problem. If the code above was connected to an MQTT node, it would
generate duplicate messages on topic B
and nothing on topic A
.
We’re changing the default approach to cloning used by the Function node to prevent this issue.
Cloning by default
With Node-RED 1.0, when a Function node calls node.send()
, it will now clone
every single message, including the first. This will ensure code like that above
will continue to work.
But unfortunately this doesn’t come for free. Cloning can be an expensive operation. For many users it won’t matter at all - their messages will be relatively small and relatively infrequent.
It will be more of an issue for flows that have very large messages, high message rates and also, more critically, flows that use messages that cannot be cloned. For example, flows that move around video frames at a high rate.
For those flows, we are introducing a new optional argument to node.send()
that
will keep the existing behaviour:
node.send(msg, false);
This will tell the Function node not to clone the message - although the rules still apply where if the flow branches, the 2nd and later branches will still get a cloned message.
Why change the default behaviour at all?
With every change we make, we have to assess the potential impact it has. The goal is to minimise that impact and keep flows working as users expect.
When this issue came up, there were two possible possible approaches we could take.
One option was to change nothing in the code and update our documentation to
explain you really shouldn’t reuse message objects and make use of
RED.util.cloneMessage()
to manually clone messages in your Function code.
The problem with this approach is it leaves users uncertain over what they should do for their flows. We have a large user base who are not experienced JavaScript developers and are not interested in the inner workings of Node-RED. They expect their flows to just work. The fact any issues this caused would be subtle and hard to track down made the potential impact pretty high.
The other option, the one we chose to take, was to change the default behaviour to ensure flows kept working for most users without them having to change anything.
One downside of this option is that there will be some flows that rely on the
non-cloning behaviour. However they will now fail in a much more obvious way;
the clone will fail with the very first message that passes through. That will
make it easier to identify the Function node at fault and to add the false
parameter as needed. In fact, they could update their flows to add the extra
parameter today in preparation of upgrading to 1.0.
The other downside is the potential performance hit of the extra clone. But again, a slight performance hit is preferable to incorrect flow behaviour. Particularly as we can seek out other ways to improve performance through-out the system as we move forward.
Given the choice of either breaking a wide range of flows in a subtle and hard to detect way, versus a clear break for a much smaller subset of flows, I hope you can see why we chose the latter.
What about other nodes?
The changes described above only apply to the Function node. Custom nodes remain
responsible for cloning messages as they need to before passing to node.send()
.