|
| 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 | + |
| 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 | + |
| 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()`. |
0 commit comments