Distributed Timers with Node.js, Dgram and multicast

O

ver the years I have had a handful of times where I have had the need for Multicasting. It usually comes about in service or node ( application ) discovery. Or if you are lucky enough, the dreaded distributed setTimeout / clearTimeout. Every time find a need for it, I spend hours in the documentation trying to remember how to use it, remembering the Node.js Docs for dgram - which are virtually void of any useful explanation or examples. Finally resorting to finding googling around for the 1 or two examples of multi casting out there and hack something together. It shouldn't like that. It is actually really easy to do with Node.js. Let's give it a shot.

Let's say for our example you have 6 instances of an application running. An end user makes an HTTP request to your application that starts some long running process. We set a timeout that will kill the operation, and email the original user that the operation has failed to complete - unless the user manually kills it with another HTTP request, in which case we would need to call clearTimeout on the timer reference.

The door to the rabbit hole here is that if the user does issue a request to cancel the job, we have no control over which server the request goes to, and as a result, we can't reliably clear the timer. Even if we were able to stop the underlying job, we'll never be able to prevent the email from going out, notifying the user that the job took too long and failed to complete.

ˈmʌltiˌkɑst -n., noun

  1. A broadcast from one source simultaneously to several receivers on a network

Node has had multicast support baked in from the beginning. While the documentation may not indicate so, it is rather simple to do. And in this situation of needing to issue a clearTimeout command to every node in a cluster - it's perfect.

// mcast.js
const dgram = require('dgram')
const socket = dgram.createSocket({reuseAddr:true,type:'udp4'});

socket.bind(45555);
socket.on('listening', function(){
    socket.setBroadcast( true );
    socket.addMembership('239.1.2.3');
});

socket.on('message', function( msg ){
    // to something here
    console.log( msg );
})

User Datagram Protocol ( UDP )

Alternative communications protocol to Transmission Control Protocol (TCP) used primarily for establishing low-latency and loss tolerating connections between applications on the Internet.

That's it! Done. You can start up as many of these as you want on different machines ( on a wired network - not WiFi ) and everyone will get every message sent be any machine. Make sure that broadcast and multicast is enabled on the network interface you are using. On Linux, type ifconfig in the terminal - You should see something like this:

enp0s3    Link encap:Ethernet  HWaddr 08:00:27:c9:c1:14  
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet6 addr: fe80::55b1:c2be:3d23:3d4b/64 Scope:Link
    --->  UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:265510 errors:0 dropped:0 overruns:0 frame:0
          TX packets:126780 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:238144376 (238.1 MB)  TX bytes:15633345 (15.6 MB)

Try it out, start a bunch of them up and then drop yourself into a Node REPL

$ node
> .load mcast.js
> socket.send("hello world", 45555, '239.1.2.3');

From here you could see how easy it would be to structure a message to look up a timer reference and cancel it if it exists. Our message could be something simple like kill:<id>

var jobs = {};

jobs[ job_id ] = setTimeout(function(){}, 1000 * 60 * 10 ).unref();

socket.on('message', function( msg ){
   var bits = msg.toString().split(':');
   var kill = bits[0] === 'kill';
   var job = jobs[ bits[1] ];
   kill && job && clearTimeout( job );
});

And you now you can kill a timer from anywhere in your cluster

> socket.send("kill:acfe001", 45555, '239.1.2.3');

Distributed clearTimeout in under 25 lines of code. Yes, it is a very simplistic example, but serves as the basis of virtually every package out on npm that does messaging over udp / multicast - service discovery / registration, Leader elections, message bus implementations, etc. This little snippet is basically what they all boil down to.

Simple. Powerful. Efficient.