18

I am new to javascript world. Recently I was working on a chat application in nodejs. So I have a method called gracefulshutdown as follows.

var gracefulShutdown = function() {
    logger.info("Received kill signal, shutting down gracefully.");
    server.close();
    logger.info('Disconnecting all the socket.io clients');
    if (Object.keys(io.sockets.sockets).length == 0) process.exit();
    var _map = io.sockets.sockets,
        _socket;
    for (var _k in _map) {
        if (_map.hasOwnProperty(_k)) {
            _socket = _map[_k];
            _socket.disconnect(true);
        }
    }
    ...code here...
    setTimeout(function() {
        logger.error("Could not close connections in time, shutting down");
        process.exit();
    }, 10 * 1000);
}

Here is what is happening in the disconnect listener.The removeDisconnectedClient method simply updates an entry in the db to indicate the removed client.

socket.on('disconnect', function() { removeDisconnectedClient(socket); });

So in this case the disconnect event wasn't fired for all sockets. It was fired for only a few sockets randomly from the array. Although I was able to fix it using setTimeout(fn, 0) with the help of a teammate.

I read about it online and understood only this much that setTimeout defers the execution of of code by adding it to end of event queue. I read about javascript context, call stack, event loop. But I couldn't put together all of it in this context. I really don't understand why and how this issue occurred. Could someone explain it in detail. And what is the best way to solve or avoid them.

Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
Narendra Rajput
  • 711
  • 9
  • 28
  • Can you provide us more context around it to help u. Are you using socket.io? what are you expecting in emitResponse?? and is the return type for socket.emit() documented somewhere the way you're expecting it.? – xdevel Jun 18 '15 at 23:53
  • Yes actually I am using socket.io library. I was expecting emitResponse to ensure that the emit was complete and it had send the message to client. I have edited the question to remove that part of code, as I went through the code to realize that socket.emit returns itself. – Narendra Rajput Jun 19 '15 at 04:25
  • 1
    You said "randomly from the array", is `_map` an array or an object? If it is an object it is okay, `for...in` is intended for use with objects, but if it is an array, [`for...in` shouldn't be used for arrays](http://stackoverflow.com/questions/500504/why-is-using-for-in-with-array-iteration-such-a-bad-idea) (read more than just the top answer, the other answers have different important info too). – Useless Code Jun 21 '15 at 08:45
  • @NarendraRajput `socket.emit` is an async function. To get result from remote you need not the return value of `emit` function but rather get in a callback function, provided as a last argument to `emit` – Kirill Slatin Jun 21 '15 at 10:13
  • @NarendraRajput check update to my answer – Kirill Slatin Jun 25 '15 at 16:35
  • Staring off... a forEach loop is not the same as a for in loop... – theodore hogberg Jun 26 '15 at 13:18

5 Answers5

4

It is hard to say for sure without a little more context about the rest of the code in gracefulShutdown but I'm surprised it is disconnecting any of the sockets at all:

_socket = _map[ _k ];
socket.disconnect(true);

It appears that you are assigning an item from _map to the variable _socket but then calling disconnect on socket, which is a different variable. I'm guessing it is a typo and you meant to call disconnect on _socket?

Some of the sockets might be disconnecting for other reasons and the appearance that your loop is disconnecting some but not all the sockets is probably just coincidence.

As far as I can tell from the code you posted, socket should be undefined and you should be getting errors about trying to call the disconnect method on undefined.

Useless Code
  • 12,123
  • 5
  • 35
  • 40
  • Yes you are right. I wanted to call disconnect on _socket. I have edited the code. And also added the complete function gracefulShutdown to give more context about what is happening in the method. Similarly I have added the listener method on socket.disconnect. I have loggers printed in the disconnect listener which get called. And if that were the case how would it work when I call the socket.disconnect with a setTimeout(fn, 0) or process.nextTick(); – Narendra Rajput Jun 21 '15 at 13:16
3

From the method name where you use it I can suppose that application exits after attempts to disconnect all sockets. The nature of socket communication is asynchronous, so given you have a decent amount of items in _map it can occur that not all messages with disconnect will be sent before the process exits.

You can increase chances by calling exit after some timeout after disconnecting all sockets. However, why would you manually disconnect? On connection interruption remote sockets will automatically get disconnected...

UPDATE
Socket.io for Node.js doesn't have a callback to know for sure that packet with disconnect command was sent. At least in v0.9. I've debugged that and came to conclusion that without modification of sources it is not possible to catch that moment.

In file "socket.io\lib\transports\websocket\hybi-16.js" a method write is called to send the disconnect packet

WebSocket.prototype.write = function (data) {
...
   this.socket.write(buf, 'binary');
...
}

Whereas socket.write is defined in Node.js core transport "nodejs-{your-node-version}-src\core-modules-sources\lib\net.js" as

Socket.prototype.write = function(chunk, encoding, cb)
//cb is a callback to be called on writeRequest complete

However as you see this callback is not provided, so socket.io will not know about the packet having been sent.

At the same time when disconnect() is called for websocket, member disconnected is set to true, and "disconnect" event is broadcasted, indeed. But synchronously. So .on('disconnect' handler on server socket doesn't give and valuable information about whether the packet was sent or not.

Solution
I can make a general conclusion from this. If it is so critical to make sure that all clients are immediately informed (and not wait for a heartbeat timeout or if heartbeat is disabled) then this logic should be implemented manually.

You can send an ordinary message which will mean for the client that server is shutting down and call socket disconnect as soon as the message is received. At the same time server will be able to accept all acknowledgements

Server-side:

var sockets = [];
for (var _k in _map) {
    if (_map.hasOwnProperty(_k)) {
        sockets.push(_map[_k]);
    }
}
sockets.map(function (socket) {
    socket.emit('shutdown', function () {
        socket.isShutdown = true;
        var all = sockets.every(function (skt) {
            return skt.isShutdown;
        });
        if (all) {
            //wrap in timeout to let current tick finish before quitting
            setTimeout(function () {
                process.exit();
            });
        }
    })
})

Clients should behave simply

socket.on('shutdown', function () {
    socket.disconnect();
});

Thus we make sure each client has explicitly disconnected. We don't care about server. It will be shutdown shortly.

Kirill Slatin
  • 6,085
  • 3
  • 18
  • 38
  • Instead of using a timeout, you can wait until the `disconnect` event has fired on all sockets. That would be cleaner and less prone to error. – Aaron Dufour Jun 24 '15 at 14:35
  • @AaronDufour I absolutely agree with you! However I haven't managed to get this scenario working. Had the same problem. Does the `disconnect` fire on the same socket that emits `disconnect`? – Kirill Slatin Jun 24 '15 at 14:49
  • @AaronDufour, could help finishing this after your remark. The idea with `on('disconnect'` confirmed to be not a solution – Kirill Slatin Jun 25 '15 at 16:29
2

In the example code it looks like io.sockets.sockets is an Object, however, at least in the library version I am using, it is a mutable array which the socket.io library is free to modify each time you are removing a socket with disconnect(true).

Thus, when you call disconnect(true); if the currently iterated item from index i is removed, this effect like this happens:

var a = [1,2,3,4];
for( var i in a) {
   a.splice(i,1); // remove item from array
   alert(i);
}
// alerts 0,1 

Thus, the disconnect(true) call will ask the socket.io to remove the item from the array - and because you are both holding reference to the same array, the contents of the array are modified during the loop.

The solution is to create a copy of the _map with slice() before the loop:

var _map = io.sockets.sockets.slice(); // copy of the original

It would create a copy of the original array and thus should go through all the items in the array.

The reason why calling setTimeout() would also work is that it would defer the removal of the items from the array, allowing the whole loop iterate without modifying the sockets -Array.

Tero Tolonen
  • 4,144
  • 4
  • 27
  • 32
1

The problem here is that sockjs and socket.io use asynchronous "disconnect" methods. IE. When you call disconnect, it is not immediately terminated. It is just a promise that it WILL be terminated. This has the following effect (assuming 3 sockets)

  1. Your for loop grabs the first socket
  2. The disconnect method is called on the first socket
  3. Your for loop grabs the second socket
  4. The disconnect method is called on the second socket
  5. The disconnect method on the first socket finishes
  6. Your for loop grabs the third socket
  7. The disconnect method is called on the third socket
  8. Program kills itself

Notice, that sockets 2 and 3 haven't necessarily finished yet. This could be for a number of reasons.

Finally, setTimeout(fn, 0) is, as you said, blocking the final call, but it may not be consistent (I haven't dug into this too much). By that I mean, you've set the final termination to be AFTER all your sockets have disconnected. The setTimeout and setInterval methods essentially act more like a queue. Your position in the queue is dictated by the timer you set. Two intervals set for 10s each, where they both run synchronously will cause one to run AFTER the other.

Angelo R.
  • 2,285
  • 1
  • 17
  • 22
  • In my case the program wasn't killing itself immediately but waited on the 10 seconds setTimeout just before process.exit at the end. However it called disconnect only for some. Now isn't the time for socket 2 and 3 to get disconnected enough. Although after adding process.nextTick/setTimeout for disconnect actually disconnected all the sockets within few milliseconds. It seems that for some reason socket.disconnect is not getting called at all. And I can say that because I had a logger as first line in my disconnect which didn't get printed. And I am using the 1.3.5 version of socket.io – Narendra Rajput Jun 25 '15 at 21:57
1

After Socket.io 1.0, the library does not expose you an array of the connected sockets. You can check that io.socket.sockets.length, is not equal to the open socket objects. Your best bet is that you broadcast a 'disconnect' message to all the clients that you want to off, and on.'disconnect' on the client side close the actual WebSocket.

a_kats
  • 191
  • 7