Skip to content

Commit 4f2067b

Browse files
committed
Add health monitor
1 parent 57793fc commit 4f2067b

5 files changed

Lines changed: 419 additions & 2 deletions

File tree

src/health-monitor.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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)]))
41+
? HealthMonitor.states.HEALTHY
42+
: HealthMonitor.states.UNHEALTHY;
43+
} catch (e) {
44+
state = HealthMonitor.states.UNHEALTHY;
45+
}
46+
47+
this.updateState({ id, state });
48+
49+
this.checks[id] = setTimeout(check, interval);
50+
};
51+
52+
this.checks[id] = setTimeout(check, 0);
53+
54+
log.info(`New health monitor check added with id '${id}'`);
55+
}
56+
57+
/**
58+
* Cleanup health monitor by clearing all timers and resetting the internal state.
59+
*/
60+
61+
cleanup() {
62+
Object.values(this.checks).forEach(clearTimeout);
63+
64+
this.checks = {};
65+
this.globalState = HealthMonitor.states.UNKNOWN;
66+
this.states = {};
67+
}
68+
69+
/**
70+
* Handles state changes.
71+
*/
72+
73+
updateState({ id, state }) {
74+
if (this.states[id] === state) {
75+
return;
76+
}
77+
78+
log.info({ id, newState: state, oldState: this.states[id] }, 'Component health status has changed');
79+
80+
this.states[id] = state;
81+
82+
// The sorted states array makes it so that the state at the end of the array is the relevant one.
83+
// The global state is:
84+
// - UNKNOWN if one exists.
85+
// - UNHEALTHY if one exists and there are no UNKNOWN states.
86+
// - HEALTHY if there are no UNKNOWN and UNHEALTHY states.
87+
const [globalState] = Object.values(this.states).sort((left, right) => {
88+
return left < right ? 1 : -1;
89+
});
90+
91+
if (this.globalState === globalState) {
92+
return;
93+
}
94+
95+
log.info({ newState: globalState, oldState: this.globalState }, 'Global health status has changed');
96+
97+
this.globalState = globalState;
98+
}
99+
}
100+
101+
/**
102+
* Health states.
103+
*/
104+
105+
HealthMonitor.states = {
106+
HEALTHY: 'healthy',
107+
UNHEALTHY: 'unhealthy',
108+
UNKNOWN: 'unknown'
109+
};
110+
111+
/**
112+
* Export `HealthMonitor` class.
113+
*/
114+
115+
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 utils = require('./utils');
89

910
/**
@@ -24,13 +25,22 @@ class ProcessManager {
2425
constructor() {
2526
this.errors = [];
2627
this.forceShutdown = utils.deferred();
28+
this.healthMonitor = new HealthMonitor();
2729
this.hooks = [];
2830
this.log = utils.getDefaultLogger();
2931
this.running = [];
3032
this.terminating = false;
3133
this.timeout = 30000;
3234
}
3335

36+
/**
37+
* Add health monitor check.
38+
*/
39+
40+
addHealthCheck(...args) {
41+
this.healthMonitor.addCheck(...args);
42+
}
43+
3444
/**
3545
* Add hook.
3646
*/
@@ -79,6 +89,17 @@ class ProcessManager {
7989
process.exit();
8090
}
8191

92+
/**
93+
* Get health monitor status.
94+
*/
95+
96+
getHealthStatus() {
97+
return {
98+
global: this.healthMonitor.globalState,
99+
individual: this.healthMonitor.states
100+
};
101+
}
102+
82103
/**
83104
* Call all handlers for a hook.
84105
*/
@@ -190,6 +211,7 @@ class ProcessManager {
190211
.then(() => this.log.info('All running instances have stopped'))
191212
.then(() => this.hook('drain'))
192213
.then(() => this.log.info(`${this.hooks.filter(hook => hook.type === 'drain').length} server(s) drained`))
214+
.then(() => this.healthMonitor.cleanup())
193215
.then(() => this.hook('disconnect'))
194216
.then(() =>
195217
this.log.info(`${this.hooks.filter(hook => hook.type === 'disconnect').length} service(s) disconnected`)

0 commit comments

Comments
 (0)