Search code examples
node.jssocket.iomobile-safarinode-redisamazon-elasticache

NodeJS + Cluster + Socket.io + Redis - iOS not leaving rooms after disconnect


I'm writing a nodejs/socket.io application that uses cluster and Redis in AWS Elasticache as the RedisStore backend. The app works heavily around rooms, and i'm having a really hard time understanding why only Mobile Safari (iPad mini retina iOS7 ) cannot leave rooms it subscribes to after it emits a request to do so. Even closing the connection from the client side leaves the socket humming along on the server, and the subscription to the room intact, but other browsers can exit without a problem.

  1. NodeJS v0.10.25
  2. Socket.io v0.9.16
  3. Redis v2.8.6 (AWS Elasticache)
  4. Ubuntu 14.04 LTS (EC2 behind loadbalancer in TCP mode)

Now, in my code, i've been using the io.sockets.manager.roomClients object to iterate through and see what rooms are actually in service. This is because io.sockets.clients() reports completely inaccurate data once connections are opened and closed for a period of time.

My code is really too long to put here, and also fairly private, but here's essentially what i've got:

Server:

if (cluster.isMaster) {

    function heartbeat(){
        // io.sockets.clients() doesn't splice dropped connections off the array
        // so we have to use io.sockets.manager.roomClients to see active rooms
        for( var i in io.sockets.manager.roomClients ) {
            console.log("Client:", i, io.sockets.manager.roomClients[i] );
        }
        setTimeout(heartbeat,5000);
    }

    heartbeat();

} else {

    io.sockets.on('connection', function (socket) {

        socket.on('subscribe', function (room) {
            console.log("Starting:",room);
            socket.join(room);
        });

        socket.on('unsubscribe', function (room) {
            console.log("Leaving:",room);
            socket.leave(room);
        });

        socket.on('disconnect', function () {
            console.log("Close Shop");
        });

    });
}

The Client:

socket.emit('subscribe', 'some-room');

Server Log

Starting: some-room

And then i get a Client log with each timeout tick:

Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }
Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }
Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }
Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }
Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }

Now, the issue is here. If i unsubscribe or disconnect from a desktop browser:

socket.emit('unsubscribe', 'some-room');
Leaving: some-room

Or

socket.disconnect();
Close Shop

The ticks look like this:

Client: lhZsH1oL2vV7BML9QkSW { '': true }

Which i expect, because as we know, socket.io sucks at connection cleanup, but at least the room subscriptions are gone. However, on my tablet, after unsubscribe or disconnect, the room subscription remains in the io.sockets.manager.roomClients object:

Client: lhZsH1oL2vV7BML9QkSW { '': true, '/some-room': true }

I'm fairly new at socket programming, so I'm sure i'm missing something obvious, but has anyone had similar issues with Mobile websockets?


Solution

  • So I discovered that by using my own methods of reference counting, i can bypass needing socket.io to be accurate. Since i'm already using Redis to back my sockets through pubsub, i just create another client connection to redis and setex the socket.id's on an expiration. When my sockets unsubscribe or disconnect, i delete all the keys in Redis associated with the socket.id. This way, i have an instant snapshot of whos on, and if the key for some reason didn't get deleted on disconnect/unsub, it will expire based on what i set for the setex call.

    I'll try and remember to come back in two days to mark my own answer as correct.