Skip to content

Commit 74da681

Browse files
committed
Add health monitor
1 parent e7f0e76 commit 74da681

4 files changed

Lines changed: 408 additions & 1 deletion

File tree

src/health-monitor.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
7+
const log = require('debugnyan')('process-manager:health-monitor');
8+
const utils = require('./utils');
9+
10+
/**
11+
* `HealthMonitor`.
12+
*/
13+
14+
class HealthMonitor {
15+
/**
16+
* Constructor.
17+
*/
18+
19+
constructor() {
20+
this.checks = {};
21+
this.globalState = HealthMonitor.states.UNKNOWN;
22+
this.states = {};
23+
}
24+
25+
/**
26+
* Add health check.
27+
*/
28+
29+
addCheck({ handler, id, interval = 5000 }) {
30+
if (this.states[id]) {
31+
throw new Error('Cannot add handler since it would overwrite an existing one');
32+
}
33+
34+
this.states[id] = HealthMonitor.states.UNKNOWN;
35+
36+
const check = async () => {
37+
let state;
38+
39+
try {
40+
state = await Promise.race([handler(), utils.timeout(5000, false)]) ? HealthMonitor.states.HEALTHY : HealthMonitor.states.UNHEALTHY;
41+
} catch (e) {
42+
state = HealthMonitor.states.UNHEALTHY;
43+
}
44+
45+
this.updateState({ id, state });
46+
47+
this.checks[id] = setTimeout(check, interval);
48+
};
49+
50+
this.checks[id] = setTimeout(check, 0);
51+
52+
log.info(`New health monitor check added with id '${id}'`);
53+
}
54+
55+
/**
56+
* Cleanup health monitor by clearing all timers and resetting the internal state.
57+
*/
58+
59+
cleanup() {
60+
Object.values(this.checks).forEach(clearTimeout);
61+
62+
this.checks = {};
63+
this.globalState = HealthMonitor.states.UNKNOWN;
64+
this.states = {};
65+
}
66+
67+
/**
68+
* Handles state changes.
69+
*/
70+
71+
updateState({ id, state }) {
72+
if (this.states[id] === state) {
73+
return;
74+
}
75+
76+
log.info({ id, newState: state, oldState: this.states[id] }, 'Component health status has changed');
77+
78+
this.states[id] = state;
79+
80+
// The sorted states array makes it so that the state at the end of the array is the relevant one.
81+
// The global state is:
82+
// - UNKNOWN if one exists.
83+
// - UNHEALTHY if one exists and there are no UNKNOWN states.
84+
// - HEALTHY if there are no UNKNOWN and UNHEALTHY states.
85+
const globalState = Object.values(this.states).sort((left, right) => { return left < right ? 1 : -1; })[0];
86+
87+
if (this.globalState === globalState) {
88+
return;
89+
}
90+
91+
log.info({ newState: globalState, oldState: this.globalState }, 'Global health status has changed');
92+
93+
this.globalState = globalState;
94+
}
95+
}
96+
97+
/**
98+
* Health states.
99+
*/
100+
101+
HealthMonitor.states = {
102+
HEALTHY: 'healthy',
103+
UNHEALTHY: 'unhealthy',
104+
UNKNOWN: 'unknown'
105+
};
106+
107+
/**
108+
* Export `HealthMonitor` class.
109+
*/
110+
111+
module.exports = HealthMonitor;

src/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Module dependencies.
55
*/
66

7+
const HealthMonitor = require('./health-monitor');
78
const log = require('debugnyan')('process-manager');
89
const utils = require('./utils');
910

@@ -26,12 +27,21 @@ class ProcessManager {
2627
constructor() {
2728
this.errors = [];
2829
this.forceShutdown = utils.deferred();
30+
this.healthMonitor = new HealthMonitor();
2931
this.hooks = [];
3032
this.running = [];
3133
this.terminating = false;
3234
this.timeout = 30000;
3335
}
3436

37+
/**
38+
* Add health monitor check.
39+
*/
40+
41+
addHealthCheck(...args) {
42+
this.healthMonitor.addCheck(...args);
43+
}
44+
3545
/**
3646
* Add hook.
3747
*/
@@ -71,6 +81,17 @@ class ProcessManager {
7181
process.exit();
7282
}
7383

84+
/**
85+
* Get health monitor status.
86+
*/
87+
88+
getHealthStatus() {
89+
return {
90+
global: this.healthMonitor.globalState,
91+
individual: this.healthMonitor.states
92+
};
93+
}
94+
7495
/**
7596
* Call all handlers for a hook.
7697
*/
@@ -183,6 +204,7 @@ class ProcessManager {
183204
.then(() => log.info('All running instances have stopped'))
184205
.then(() => this.hook('drain'))
185206
.then(() => log.info(`${this.hooks.filter(hook => hook.type === 'drain').length} server(s) drained`))
207+
.then(() => this.healthMonitor.cleanup())
186208
.then(() => this.hook('disconnect'))
187209
.then(() => log.info(`${this.hooks.filter(hook => hook.type === 'disconnect').length} service(s) disconnected`))
188210
.then(() => this.hook('exit', this.errors));

0 commit comments

Comments
 (0)