Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling socket.disconnect in a forEach loop doesn't actually call disconnect on all sockets

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.

like image 399
Narendra Rajput Avatar asked Jun 18 '15 19:06

Narendra Rajput


People also ask

Does socket IO reconnect after disconnect?

Socket disconnects automatically, reconnects, and disconnects again and form a loop. #918.

Does socket emit return a promise?

emit() happens to return the socket itself (allows for method chaining). But, that's not relevant here because there's no point in returning any value from a Promise executor function. If you do return a value, it is not used by the Promise in any way.


2 Answers

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.

like image 187
Useless Code Avatar answered Oct 13 '22 13:10

Useless Code


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.

like image 3
Kirill Slatin Avatar answered Oct 13 '22 13:10

Kirill Slatin