2

I have an angularjs app, with node and express on the server side. I also have node-amqp and socket.io

I want to implement the following behaviour

The app has a page (route, angular view) that displays a table with real-time data The data are updated in real-time using socket.io and amqp to stream the data from a rabbitMQ server that sits outside the app.

When the user visits this page/route on the browser

  1. the client emits a socket event “subscribe”
  2. the server, on the socket event “subscribe”,
    • declares a rabbit queue
    • binds the rabbit queue to the exchange
    • subscribes to messages/data from the rabbit queue
    • emits a socket event “data” sending the data back to the user/client

When the user leaves the page, or in other words changes route

  1. the client emits a socket event “unsubscribe”
  2. the server, on the socket event “unsubscribe”,
    • unsubscribes from the queue

My problem is: how to ensure that the queue.subscribe and queue.unsubscribe are synchronized? If the user executes a fast sequence of route changes: visit/leave/visit/leave/visit/leave The order of subscribe and unsubscribe is sometimes reverted, and the server unsubscribes a second time the previous subscribtion before the new subscription is completed. Any suggestion? This is what I tried, but is not working:

Client side: controller.js

.controller('WatchdogCtrl', function($scope, watchSocket) {

    var data = {}
    $scope.data = []

    var socket = watchSocket

    socket.emit('subscribe', { exchange: 'bus', key: 'mis.service-state' })
    socket.on('data', function(message) {
        // refreshing  data 
        data[message.payload.id] = message.payload;
        var new-values = [];
        angular.forEach(data, function(value, index) {
            this.push(value);
        }, new-values);

        $scope.data = new-values
        $scope.$apply()
    });

    $scope.$on('$destroy', function (event) {
        // unsubscribe from rabbit queue when leaving 
        socket.emit('unsubscribe')
    });
})

Server side: server.js

// set up amqp listener
var amqp = require('amqp');
// create rabbitmq connection with amqp
var rabbitMQ = amqp.createConnection({url: "amqp://my:url"});
rabbitMQ.on('ready', function() {
    console.log('Connection to rabbitMQ is ready')
});

// Hook Socket.io into Express
var io = require('socket.io').listen(server);
io.set('log level', 2);
io.of('/watch').on('connection', function(socket) {
    var watchq;
    var defr;
    socket.on('subscribe', function(spec) {
        watchq = rabbitMQ.queue('watch-queue', function(queue) {
            console.log('declare rabbit queue: "' + queue.name +'"');
            console.log('bind queue '+ queue.name + ' to exch=' + spec.exchange + ', key=' + spec.key);

            queue.bind(spec.exchange, spec.key)
            defr = queue.subscribe(function(message, headers, deliveryInfo) {
                     socket.emit('data', {
                        key: deliveryInfo.routingKey,
                        payload: JSON.parse(message.data.toString('utf8'))
                     })
                   }).addCallback(function(ok) { 
                       var ctag = ok.consumerTag; 
                       console.log('subscribed to queue: ' + queue.name + ' ctag = ' + ctag)
                   });

        })
    })

    socket.on('unsubscribe', function() {
        //needs fix: this does not ensure subscribe/unsubscribe synchronization…..
        defr.addCallback(function(ok) {
            console.log('unsubscribe form queue:', watchq.name, ', ctag =', ok.consumerTag)
            watchq.unsubscribe(ok.consumerTag);
        })
    })

});

Server console.log messages: (visit#3 and leave#3 are out of sync)

declare rabbit queue: "watch-queue"
bind queue watch-queue to exch=bus, key=mis.service-state
subscribed to queue: watch-queue ctag = node-amqp-8359-0.6418165327049792 //<-- visit#1
unsubscribe form queue: watch-queue , ctag = node-amqp-8359-0.6418165327049792 //<--leave#1
declare rabbit queue: "watch-queue"
bind queue watch-queue to exch=bus, key=mis.service-state
subscribed to queue: watch-queue ctag = node-amqp-8359-0.455362161854282 //<-- visit#2
unsubscribe form queue: watch-queue , ctag = node-amqp-8359-0.455362161854282 //<-- leave#2
unsubscribe form queue: watch-queue , ctag = node-amqp-8359-0.455362161854282 //<-- leave#3
declare rabbit queue: "watch-queue"
bind queue watch-queue to exch=bus, key=mis.service-state
subscribed to queue: watch-queue ctag = node-amqp-8359-0.4509762797970325 //<-- visit#3
klode
  • 10,821
  • 5
  • 34
  • 49

1 Answers1

3

We have a very similar setup as yours. We create an anonymous, exclusive queue with an expire time if unused. Anonymous queues get a unique name generated for them by the broker. Exclusive queues are deleted as soon as the client disconnects (as soon as the channel is torn down). Expire time for queues is a RabbitMQ extension but supported by amqplib which we use. I'm sure node-amqp also have some kind of support for such extensions.

Also create a channel (but reuse the same connection) for each socket. This gives a one-to-one mapping between a socket and an anonymous queue. Any bindings to that queue is equivalent to a binding for a single socket. Because of this we inherently knows what socket should get what messages, without any special naming convention for queues or checking of routing keys, etc.

Close the RabbitMQ channel (again, not the connection) when the socket is closed. No need for a special unsubscribe event although we might add such an event as well at a later date.

This also means that the same browser can have multiple queues if they have multiple tabs open without any race conditions.

Emil Vikström
  • 90,431
  • 16
  • 141
  • 175