Skip to content

Commit d07ddb2

Browse files
committed
Add cloning post
1 parent 378d751 commit d07ddb2

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed

_posts/2019-09-13-cloning-messages.md

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
layout: blog
3+
title: Cloning messages in a flow
4+
author: nick
5+
description: With the move to asynchronous messaging, we're also changing how the Function node clones messages. Find out what cloning is all about, why it's necessary and what's changing in 1.0.
6+
---
7+
8+
With the change to asynchronous message passing in Node-RED 1.0, we're also changing
9+
how some messages are cloned between nodes. The behaviour in this area wasn't
10+
always clear and could lead to unexpected results to end users who weren't familiar
11+
with some of the principles of JavaScript object handling.
12+
13+
This post explains what cloning messages is all about, why it's necessary and what
14+
is changing in 1.0.
15+
16+
17+
### Pass-by-reference
18+
19+
Before we get into the details of cloning messages, we once again take a detour
20+
into how JavaScript works.
21+
22+
Let's consider the following code:
23+
24+
```javascript
25+
> let a = { payload: "hello" };
26+
> let b = a;
27+
> b.payload = "goodbye";
28+
> console.log(a)
29+
{ payload: 'goodbye' }
30+
```
31+
32+
We create a new object and assign it to the variable `a`. We then assign variable
33+
`b` to the value of `a`. Next we change the value of `b.payload`. Finally we print
34+
the original variable `a`.
35+
36+
As if by magic, the change we made to variable `b` has been made to variable `a`
37+
as well. This is because we did *not* make a copy of the object - we created a new
38+
reference to the same object in memory.
39+
40+
This is known as pass-by-reference and can cause unexpected results if you aren't
41+
prepared for it.
42+
43+
### Cloning messages
44+
45+
Within a Node-RED flow, when a node receives a message (which is a JavaScript
46+
Object), it is free to modify the message however it wants and then pass it on
47+
to any nodes it is connected to.
48+
49+
![](/blog/content/images/2019/09/flow1.svg)
50+
51+
When Node A sends a message it generates two 'send events' - one for Node B and
52+
one for Node C. If we simply passed the message to Node B then to Node C, any
53+
modifications to the message made by Node B would be visible to Node C.
54+
55+
This is where the need to clone messages comes in. Node-RED automatically clones
56+
messages before they are passed on in order to prevent this type of cross-branch
57+
modification.
58+
59+
But it isn't quite as simple as that. Cloning messages can be expensive to do. For
60+
flows that are single row of nodes with no branching, there is in general no need
61+
to do any cloning of messages.
62+
63+
![](/blog/content/images/2019/09/flow2.svg)
64+
65+
So the code tries to optimise when it will clone a message or not. The algorithm is:
66+
67+
> When `node.send()` is called it generates a list of send events. The first
68+
> send event uses the message object given as-is. All of the remaining send events
69+
> clone their message before passing it on.
70+
71+
Essentially that means, for a node wired to one other node, a call to
72+
`node.send(msg)` will *not* clone that message because there is no need to.
73+
74+
But this algorithm has its limits. In particular, we cannot handle the case
75+
where a Function node calls `node.send()` multiple times with the *same*
76+
message object.
77+
78+
For example, consider the following code from a Function node:
79+
80+
```javascript
81+
msg.topic = "A";
82+
msg.payload = "1";
83+
node.send(msg);
84+
85+
msg.topic = "B";
86+
msg.payload = "2";
87+
node.send(msg);
88+
```
89+
90+
Each individual call to `node.send()` will generate one send event, so the message
91+
does not get cloned.
92+
93+
For *some* flows, this was not a problem prior to Node-RED 1.0. If the latter nodes
94+
in the flow were entirely synchronous, the first call to `node.send()` would pass
95+
the whole way down the flow and complete before the execution would return to the
96+
function and modify the message for the second call.
97+
98+
But with Node-RED 1.0 introducing fully asynchronous message passing, this pattern
99+
of code would potentially be unsafe to use. The message would get modified *before*
100+
the send event from the first `node.send()` call is delivered.
101+
102+
This can cause quite subtle issues that are hard spot. There's no loud bang to alert
103+
the user to a problem. If the code above was connected to an MQTT node, it would
104+
generate duplicate messages on topic `B` and nothing on topic `A`.
105+
106+
We're changing the default approach to cloning used by the Function node to prevent
107+
this issue.
108+
109+
### Cloning by default
110+
111+
With Node-RED 1.0, when a Function node calls `node.send()`, it will now clone
112+
every single message, including the first. This will ensure code like that above
113+
will continue to work.
114+
115+
But unfortunately this doesn't come for free. Cloning *can* be an expensive operation.
116+
For many users it won't matter at all - their messages will be relatively small
117+
and relatively infrequent.
118+
119+
It will be more of an issue for flows that have very large messages, high message
120+
rates and also, more critically, flows that use messages that *cannot be cloned*.
121+
For example, flows that move around video frames at a high rate.
122+
123+
For those flows, we are introducing a new optional argument to `node.send()` that
124+
will keep the existing behaviour:
125+
126+
```javascript
127+
node.send(msg, false);
128+
```
129+
130+
This will tell the Function node *not* to clone the message - although the rules
131+
still apply where if the flow branches, the 2nd and later branches *will* still
132+
get a cloned message.
133+
134+
### Why change the default behaviour at all?
135+
136+
With every change we make, we have to assess the potential impact it has. The goal
137+
is to minimise that impact and keep flows working as users expect.
138+
139+
When this issue came up, there were two possible possible approaches we could take.
140+
141+
One option was to change nothing in the code and update our documentation to
142+
explain you really shouldn't reuse message objects and make use of
143+
`RED.util.cloneMessage()` to manually clone messages in your Function code.
144+
145+
The problem with this approach is it leaves users uncertain over what they should
146+
do for *their* flows. We have a large user base who are not experienced JavaScript
147+
developers and are not interested in the inner workings of Node-RED. They expect
148+
their flows to just work. The fact any issues this caused would be subtle and
149+
hard to track down made the potential impact pretty high.
150+
151+
The other option, the one we chose to take, was to change the default behaviour to
152+
ensure flows kept working for most users without them having to change *anything*.
153+
154+
One downside of this option is that there will be some flows that rely on the
155+
non-cloning behaviour. However they will now fail in a much more obvious way;
156+
the clone will fail with the very first message that passes through. That will
157+
make it easier to identify the Function node at fault and to add the `false`
158+
parameter as needed. In fact, they could update their flows to add the extra
159+
parameter *today* in preparation of upgrading to 1.0.
160+
161+
The other downside is the potential performance hit of the extra clone. But again,
162+
a slight performance hit is preferable to incorrect flow behaviour. Particularly as
163+
we can seek out other ways to improve performance through-out the system as we
164+
move forward.
165+
166+
Given the choice of either breaking a wide range of flows in a subtle and hard to
167+
detect way, versus a clear break for a much smaller subset of flows, I hope
168+
you can see why we chose the latter.
169+
170+
### What about other nodes?
171+
172+
The changes described above *only* apply to the Function node. Custom nodes remain
173+
responsible for cloning messages as they need to before passing to `node.send()`.

blog/content/images/2019/09/flow1.svg

+76
Loading

blog/content/images/2019/09/flow2.svg

+48
Loading

css/blog.css

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@
8989
margin-top: 5px;
9090
color: #666;
9191
font-size: 0.9em;
92+
overflow: hidden;
93+
max-height: 180px;
9294
}
9395
.post-preview p {
9496
margin: 0;

0 commit comments

Comments
 (0)