Here's how I solved it using Redis scripts. This requires version 2.6 or later, so it is likely that you still need to compile your own instance.
Every time the process starts, I generate a new UUID and leave it in the global scope. I could use pid, but this is a little better.
# Pardon my coffeescript processId = require('node-uuid').v4()
When a user connects (socket.io connection event), I then push this user id into the list of users based on this processId. I also set the expiration of this key to 30 seconds.
RedisClient.lpush "process:#{processId}", user._id RedisClient.expire "process:#{processId}", 30
When the user disconnects (disconnect event), I delete him and update the expiration date.
RedisClient.lrem "process:#{processId}", 1, user._id RedisClient.expire "process:#{processId}", 30
I also set up a function that runs on a 30 second interval to essentially “ping” this key so that it stays there. Therefore, if the process accidentally dies, all of these user sessions will substantially disappear.
setInterval -> RedisClient.expire "process:#{processId}", 30 , 30 * 1000
Now for the magic. Redis 2.6 includes LUA scripts, which essentially provide the functionality of a stored procedure. This is very fast and not very intensive for the processor (they compare it with the "almost" running C code).
My stored procedure basically goes through all the process lists and creates the user: user_id key with the total number of current logins. This means that if they are logged in with two browsers, etc., it will still allow me to use the logic to find out if they are completely disconnected or just one of their sessions.
I run this function every 15 seconds in all my processes, and also after the connect / disconnect event. This means that my user’s count will most likely be accurate for the second one and will never be more than 15-30 seconds.
The code for generating this redis function is as follows:
def = require("promised-io/promise").Deferred reconcileSha = -> reconcileFunction = " local keys_to_remove = redis.call('KEYS', 'user:*') for i=1, #keys_to_remove do redis.call('DEL', keys_to_remove[i]) end local processes = redis.call('KEYS', 'process:*') for i=1, #processes do local users_in_process = redis.call('LRANGE', processes[i], 0, -1) for j=1, #users_in_process do redis.call('INCR', 'user:' .. users_in_process[j]) end end " dfd = new def() RedisClient.script 'load', reconcileFunction, (err, res) -> dfd.resolve(res) dfd.promise
And then I can use this in my script later:
reconcileSha().then (sha) -> RedisClient.evalsha sha, 0, (err, res) ->
The last thing I do is try to handle some shutdown events to make sure that the process tries not to rely on redis timeouts and is actually gracefully shutting down.
gracefulShutdown = (callback) -> console.log "shutdown" reconcileSha().then (sha) -> RedisClient.del("process:#{processId}") RedisClient.evalsha sha, 0, (err, res) -> callback() if callback?
While it works fine.
One thing I still want to do is make the redis function return any keys that have changed their values. That way, I could really send an event if the counts changed for a specific user without any active servers (for example, if the process dies). At the moment, I have to rely on a user survey: * Values again to know that it has changed. It works, but it could be better ...