Drover is native nodejs solution that takes away pain when orchestrating composite application and providers all-in-one point for graceful cluster control
Key features:
- run and manage multiple app processes as simple app
- runtime scale
- graceful reload (helpful for zero-downtime releases)
Using npm:
$ npm i --save droverUsing yarn:
$ yarn add drovermain.js
const { MasterFactory } = require('drover');
const master = MasterFactory.create(
{
script: 'path/to/app.js',
}
);
process.on('SIGINT', async () => {
await master.gracefulShutdown();
process.exit(0);
});
master.start().catch(console.error);app.js
const express = require('express');
const { MessageBus } = require('drover');
const mb = new MessageBus();
mb.on('stop', () => {
server.close(mb.sendStopped);
});
mb.on('quit', () => {
mb.sendShuttedDown();
setTimeout(() => process.exit(0), 50);
});
const app = express().get('/', (req, res) => {
res.send('hello');
});
const server = app.listen(1999, mb.sendStarted);- Main context (facade) - facade part of application, which is responsible for launching logic and managing the lifecycle of functional parts of the application
- Functional context (business logic) - functional parts of application that directly contain the business logic
- Master - entity of drover, which takes the role of creating and maintaining a given number of workers. This entity is always located only in
main contextof application. - Worker - entity of drover, which assumes a role in the actual up-to-date presentation of state of
functional contextprocess onmain contextside. - MessageBus - entity of drover, which connects the
functional contextof application withmain contextprovides bidirectional commands via IPC channel. Strongly associated withWorkerand appear as it's representation on side of thefunctional context.
-
in
main contextwe should control worker processes of application. Here we can describe the logic of restarting falling workers, log them or even implement API that will allow us change workers state from the outside process context. E.g. to bring the workers in maintenance mode and suspend the application without stopping the master process itself. -
in
main context, we get a guarantee that all workers have successfully completed the start, according to the internal business logic of the functional parts of the application. When you use defaultclustermodule, the only signal for you about raising workers is to establish a IPC channel betweenworkerandmaster. This is not very convenient and not very informative. Indeed, it does not guarantee that the worker is started up functionally correct up to bootstrap steps. -
in
main context, we get access to the direct signaling offunctional context, including the ability to correctly stop worker process or perform full controlled restart
- Logic segregation -
main contextof the applicationMUST NOTcontain business logic and vice versa -functional contextof the applicationMUST NOTcontain logic of monitoring and controlling lifecycle of composite parts of the application itself.
To distinguish exit reasons you may import ExitReasons object.
const { ExitReasons } = require('drover');There are such available classes:
-
ExitReasons.ExternalSignal -
ExitReasons.NormalExit -
ExitReasons.AbnormalExit
Underlying process may not start at all, it may fail after some time or it may be killed with signal. Each instance has its own reason-specific payload field. There is a list of reasons:
-
ExitReasons.ExternalSignal- worker process was killed by someone or by another process with the signal, the signal name can be found inpayload.signalfield. -
ExitReasons.NormalExit- worker process has exited with0code.payload.codeis provided -
ExitReasons.AbnormalExit- worker process has exited with non-zero code.payload.codeis provided.
FYI: You can find and run every case listed below in examples.
In this case, the logic in the approach will be absolutely identical to the usual use of the cluster nodejs module. However, in the case of a drover, we get a number of important advantages and convenience of working with a complex application using understandable entities and a transparent interface.
- Create instance of
Master
const master = MasterFactory.create(
{
script: 'path/to/app.js', // required
count: 4, // optional
env: { // optional
PORT: 1934
},
}
);- Add listener on
worker-exitevent ofMaster
master.on('worker-exit', async (reason, workerId) => {
if (reason instanceof ExitReasons.ExternalSignal || reason instanceof ExitReasons.AbnormalExit) {
// restart worker if something abnormal happened or external process killed worker by signal
await master.restartWorkerById(workerId);
} else {
// for different cases just hard quit all app
const { code, signal } = reason.payload;
// quit method will be described in next section
await quit(code, signal, true);
}
});- Handle terminating of
main contextprocess
const quit = async (code, signal, force = false) => {
try {
if (force) {
// in case of emergency stop we just hard quit all worker processes
await master.hardShutdown();
} else {
// for default app exit we do it in more graceful way
await master.gracefulShutdown();
}
} catch (err) {
// your stop-failed handler logic here
console.error(err);
}
setTimeout(() => process.exit(0), 0);
};
// handle main process SIGINT (default signal in Unix when "ctrl+c" terminal interruption happened)
process.on('SIGINT', quit);- Start application. This part will fork
functional context(app.js) and run 4 instances of it.
const run = async () => {
try {
await master.start();
// right here we have a guarantee that all 4 app instances
// already did their business logic (raised connects, started servers, etc)
} catch (err) {
// your start-failed handler logic here
console.error(err);
return;
}
// primitive health-check from master every 2s
setInterval(() => {
master.getWorkersStatuses().forEach((v, i) => {
console.log(`[app-${i}]: ${statusMap[v]}`);
});
console.log('---');
}, 2000);
};
run().catch(console.error);- Create instance of
MessageBus:
const mb = new MessageBus();- Setup listeners on
stopandquitevents:
mb.on('stop', () => {
server.close(mb.sendStopped);
});
mb.on('quit', () => {
mb.sendShuttedDown();
setTimeout(() => process.exit(0), 50);
});- Send start signal to master when all bootstrap part is done:
const app = express().get('/', (req, res) => {
res.send(RESPONSE);
});
const server = app.listen(PORT, mb.sendStarted);For that example above we will expect next output in console repeated every 2s:
[app-0]: STARTED
[app-1]: STARTED
[app-2]: STARTED
[app-3]: STARTED
---In this case, the logic will same as if you are using several cluster modules within one main context without loss of ease of management.
Most parts of context building are similar to previous example, but with little different points.
Here we instantiate N different masters.
const fooMaster = MasterFactory.create(
{
script: 'path/to/app-foo.js',
count: 2,
env: {
PORT: 2100,
RESPONSE: 'multi-app-cluster'
}
}
);
const barMaster = MasterFactory.create(
{
script: 'path/to/app-bar.js',
count: 2,
env: {
PORT: 2101,
RESPONSE: 'multi-app-cluster'
}
}
);
const bazMaster = MasterFactory.create(
{
script: 'path/to/app-baz.js',
count: 4,
env: {
PORT: 2102,
RESPONSE: 'multi-app-cluster'
}
}
);After that we subscribe listeners on worker-exit event and start all masters.
You can start them parallel with Promise.all, or consistently one by one. It totally depends on your business logic.
If you don't need cluster multi-instances of your app, you still can run you application with drover. Most parts of
context building are similar to first example with little difference in master's option count when you instantiate it.
const master = MasterFactory.create(
{
script: 'path/to/app.js',
count: 1
}
);In this case you still got advantages of graceful reloads with zero-downtime of your app. When reload begins - one more instance of app will be added right before previous one shut down.
drover covered with MIT license, so it's free to use for any kind of your private or commercial projects without
any restrictions and obligations to be open-sourced.
pm2 has programmatic flow, but it is still just API to pm2 demon process and it brings some usage restrictions.
With drover you've got more options, so flexibility for your business logic raises a lot.
pm2 uses "let if fail" concept, but drover gives you control instead. You've got not just exits as fact, but you can
manage different ExitReasons and handle each case according to your needs.
drover module use debug module for this this purpose.
Just run your app with DEBUG env var like example below:
DEBUG=drover:* node main.jsSample output for simple-app start:
And changes with SIGINT signal to main.js process triggered by Ctrl+C:
As you see, you have transparent access to all events, state changes and errors described behaviour even via IPC communication.