Un sencillo servidor corriendo sobre Node.js puede permitir la recarga sin interrupciones, de manera amistosa, algo parecido al reload de Apache u otros demonios en Linux. Para ello sólo es necesario que haya un proceso padre que lance hijos y les envíe señales según sea necesario. Sin embargo el cierre de las conexiones esperando a que estas terminen puede dar algunas complicaciones cuando hay un WebSocket.
La gestión de padre e hijos se puede hacer con el módulo cluster. En este ejemplo el script a lanzar manualmente es master.js. Él a su vez lanzará al script server.js y le pasará las señales SIGHUP para recargar el servidor (terminar amistosamente) o SIGTERM para terminar sin piedad.
master.js:
'use strict'; var cluster = require('cluster'); const path = require('path'); cluster.setupMaster({ exec: path.join(__dirname, 'server.js') }); //fork the first process cluster.fork(); process.on('SIGHUP', function () { var new_worker = cluster.fork(); new_worker.once('listening', function () { //stop all other workersS for (var id in cluster.workers) { if (id === new_worker.id.toString()) continue; cluster.workers[id].process.kill('SIGHUP'); } }); }).on('SIGTERM', function () { for (var id in cluster.workers) { cluster.workers[id].process.kill('SIGTERM'); } });
El comportamiento un tanto especial está cuando recibe la señal SIGHUP. En ese caso el proceso padre primero lanza un nuevo hijo, se espera a que esté listo (escuchando) y entonces le transmite la señal al hijo que ya estaba arrancado, cuidando de no enviársela también al nuevo.
server.js:
'use strict'; const express = require('express'); const app = express(); var server = require('http').createServer(app); const ServerShutdown = require('server-shutdown'); const serverShutdown = new ServerShutdown(); const io = require('socket.io')(server); serverShutdown.registerServer(io, ServerShutdown.Adapters.socketio); const spawn = require('child_process').spawn; var messages = [{ author: "Carlos", text: "Hola! que tal?" }]; var sockets = []; app.use(express.static('public')); app.get('/hello', function (req, res) { res.status(200).send("Hello World!"); }); app.get('/sleep', function (req, res) { const ls = spawn('sleep', ['10']); ls.on('close', (code) => { res.send('wake up!\n'); }); }); io.on('connection', function (socket) { sockets.push(socket); socket.emit('messages', messages); socket.on('new-message', function (data) { messages.push(data); io.sockets.emit('messages', messages); }); }); process.on('SIGHUP', _ => { serverShutdown.shutdown(_ => { process.exit(0); }); }).on('SIGTERM', _ => { serverShutdown.forceShutdown(_ => { process.exit(0); }); }); server.listen(8100); serverShutdown.registerServer(server);
Haciendo un GET a http://localhost:8100/sleep y enviando mientras tanto una señal SIGHUP al proceso padre se puede ver cómo:
- Se arranca un servidor nuevo.
- No se interrumpe la petición del antiguo.
- Si se lanzan nuevas peticiones ya son atendidas por el servidor nuevo.
- Al terminar la petición del servidor antiguo este terminará.
No obstante lo más interesante está con el uso del WebSocket. Sin un WebSocket sería tan fácil como llamar a server.close y el servidor se limitaría a no aceptar conexiones nuevas y terminar cuando las ya establecidas terminen. Esto último suena muy amistoso de cara a no interrumpir las peticiones que se estén atendiendo, pero con un WebSocket da problemas, porque se trata de una conexión ya establecida que puede durar lo que el cliente tenga abierto el navegador y esté enviando keep-alives, que son los que realmente crean el problema.
Ahí es donde entra en juego el módulo server-shutdown, que controla el estado de las conexiones incluso si son WebSockets. Para el caso de un reload ofrece la función shutdown, que se encargará de forzar el cierre de las conexiones que no estén transmitiendo/recibiendo datos, lo que permite cerrar el servidor en cuanto se pueda y de manera amistosa. Pero también ofrece la función forceShutdown para forzar el cierre del servidor sin esperar a nada, como haría un stop o disable al demonio Apache, muy útil para terminar la aplicación de inmediato.
El ejemplo completo se encuentra en GitHub.
He hecho este ejemplo combinando dos:
- Reloading node with no downtime José F. Romaniello.
- WebSockets: Cómo utilizar Socket.io en tus aplicaciones web Carlos Azaustre.