Así es como lo resolví usando Redis scripting. Requiere la versión 2.6 o posterior, por lo que probablemente aún requiera compilar su propia instancia por el momento.
Cada vez que se inicia un proceso, genero un nuevo UUID y lo dejo en el alcance global. Podría usar el pid, pero esto se siente un poco más seguro.
# Pardon my coffeescript
processId = require('node-uuid').v4()
Cuando un usuario se conecta (el evento de conexión socket.io), que luego empuje Identificación de ese usuario en una lista de usuarios en función de que processId. También establecí el vencimiento de esa clave en 30 segundos.
RedisClient.lpush "process:#{processId}", user._id
RedisClient.expire "process:#{processId}", 30
Cuando un usuario se desconecta (el evento de desconexión), lo elimino y actualizo la caducidad.
RedisClient.lrem "process:#{processId}", 1, user._id
RedisClient.expire "process:#{processId}", 30
También configuro una función que se ejecuta en un intervalo de 30 segundos para esencialmente "hacer ping" esa clave para que permanezca allí. Entonces, si el proceso muere accidentalmente, todas esas sesiones de usuario desaparecerán esencialmente.
setInterval ->
RedisClient.expire "process:#{processId}", 30
, 30 * 1000
Ahora para la magia. Redis 2.6 incluye secuencias de comandos LUA, que básicamente proporciona una especie de funcionalidad de procedimiento almacenado. Es realmente rápido y no requiere mucho del procesador (lo comparan con "casi" ejecutar el código C).
Mi procedimiento almacenado recorre básicamente todas las listas de procesos y crea una clave user_id de usuario con su recuento total de inicios de sesión actuales. Esto significa que si están conectados con dos navegadores, etc., todavía me permitirá usar la lógica para decir si se han desconectado por completo, o solo una de sus sesiones.
Ejecuto esta función cada 15 segundos en todos mis procesos, y también después de un evento de conexión/desconexión. Esto significa que mis recuentos de usuarios probablemente serán precisos al segundo y nunca incorrectos durante más de 15 a 30 segundos.
El código para generar esa función Redis parece:
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
y luego puedo utilizar eso en mi guión más tarde con:
reconcileSha().then (sha) ->
RedisClient.evalsha sha, 0, (err, res) ->
# do stuff
La última cosa que hago es tratar de manejar algunos eventos de apagado para asegurarse de que el proceso intente lo mejor es no confiar en los tiempos de espera de redis y en realidad se cierra con gracia.
gracefulShutdown = (callback) ->
console.log "shutdown"
reconcileSha().then (sha) ->
RedisClient.del("process:#{processId}")
RedisClient.evalsha sha, 0, (err, res) ->
callback() if callback?
# For ctrl-c
process.once 'SIGINT', ->
gracefulShutdown ->
process.kill(process.pid, 'SIGINT')
# For nodemon
process.once 'SIGUSR2', ->
gracefulShutdown ->
process.kill(process.pid, 'SIGUSR2')
Hasta ahora ha estado funcionando genial.
Una cosa que todavía quiero hacer es hacer que la función redis devuelva cualquier clave que haya cambiado sus valores. De esta forma, podría enviar un evento si los conteos han cambiado para un usuario en particular sin que ninguno de los servidores lo sepa activamente (como si un proceso muere). Por ahora, tengo que confiar en sondear al usuario: * valores nuevamente para saber que ha cambiado. Funciona, pero podría ser mejor ...
¿Por qué no solo consultar cada uno de los servidores y sumar la cantidad de clientes conectados? – k00k
Yo también estoy buscando una manera de responder a esta pregunta, sin tener que configurar algún tipo de observador para ello. FWIW, sin embargo, parece que la lógica es que cada servidor conoce a todos los clientes conectados a todos los servidores, pero también puede tener clientes obsoletos que se desconectaron de otro servidor. Parece que socket.io no creía que valiera la pena sobrecargar servidores antiguos en otros servidores, sino que algunos servidores simplemente transmitirán a algunos vacíos. – Konklone