UNPKG

11.3 kBJavaScriptView Raw
1const EventEmitter = require('events')
2
3const precond = require('precond')
4const isObject = require('lodash.isobject')
5const cloneDeep = require('lodash.clonedeep')
6const humanInterval = require('human-interval')
7
8const promiseAllSilent = require('./util/promise-all-silent')
9const promiseSeriesSilent = require('./util/promise-series-silent')
10
11module.exports = (Dependency) => {
12 /**
13 * @class
14 * @extends EventEmitter
15 */
16 class Copacetic extends EventEmitter {
17 /**
18 * @param {String} [name=''] The name of your service
19 */
20 constructor (name = '', eventEmitterMode = true) {
21 super()
22
23 this.isPolling = false
24 this.name = name
25 this.eventEmitterMode = eventEmitterMode
26 this.dependencyNames = []
27 this.dependencyIndex = {}
28 }
29
30 /**
31 * @return {Boolean} Copacetic is healthy when all hard dependencies are healthy
32 */
33 get isHealthy () {
34 return !this
35 .dependencyNames.map(s => this.getDependency(s).healthSummary)
36 .some(health => health.level === 'HARD' && health.healthy === false)
37 }
38
39 /**
40 * @return {Array<DependencyHealth>} Health information on all dependencies
41 */
42 get healthInfo () {
43 return this.dependencyNames.map(s => this.getDependency(s).healthSummary)
44 }
45
46 /**
47 * @return {HealthReport} A full report of health information and dependencies
48 */
49 get healthReport () {
50 /**
51 * The full health report including isHealthy and dependencies
52 * @typedef {Object} HealthReport
53 * @property {Boolean} isHealthy The result of {@link Copacetic#isHealthy}
54 * @property {String} Name
55 * @property {Array<DependencyHealth>} dependencies The result of {@link Copacetic#healthInfo}
56 */
57 return {
58 name: this.name,
59 isHealthy: this.isHealthy,
60 dependencies: this.healthInfo
61 }
62 }
63
64 /**
65 * @param {Dependency|String} dependency
66 * @return {Dependency}
67 */
68 getDependency (dependency) {
69 if (isObject(dependency)) {
70 return this.dependencyIndex[dependency.name]
71 }
72
73 return this.dependencyIndex[dependency]
74 }
75
76 /**
77 * @param {Dependency|String} dependency
78 * @return {Boolean} Whether the dependency has been registered
79 */
80 isDependencyRegistered (dependency) {
81 return !!this.getDependency(dependency)
82 }
83
84 /**
85 * Adds a dependency to a Copacetic instance
86 * @param {Object} opts The configuration for a dependency
87 * @return {Copacetic}
88 */
89 registerDependency (opts) {
90 const dependency = Dependency(opts)
91
92 precond.checkState(
93 this.isDependencyRegistered(dependency) === false,
94 `Dependency ${dependency.name} was already registered`
95 )
96
97 this.dependencyNames.push(dependency.name)
98 this.dependencyIndex[dependency.name] = dependency
99
100 return this
101 }
102
103 /**
104 * Removes a dependency from a Copacetic instance
105 * @param {String} name The name used to identify a dependency
106 * @return {Copacetic}
107 */
108 deregisterDependency (dependency) {
109 const resolved = this.getDependency(dependency)
110
111 precond.checkIsDef(
112 resolved,
113 `Tried to deregister dependency, but dependency is not registered`
114 )
115 resolved.cleanup()
116
117 delete this.dependencyIndex[resolved.name]
118 this.dependencyNames = this.dependencyNames.filter(n => n !== resolved.name)
119
120 return this
121 }
122
123 /**
124 * Polls the health of all registered dependencies
125 * @param {String} [interval='5 seconds']
126 * @param {Boolean} [parallel=true]
127 * @param {String} [schedule='start']
128 * @return {Copacetic}
129 */
130 pollAll ({ interval, parallel, schedule } = {}) {
131 this.poll({ dependencies: 'all', interval, parallel, schedule })
132
133 return this
134 }
135
136 /**
137 * Polls the health of a set of dependencies
138 * @example
139 * copacetic.poll({
140 * dependencies: [
141 * { name: 'my-dep' },
142 * { name: 'my-other-dep', retries: 2, maxDelay: '2 seconds' }
143 * ],
144 * schedule: 'end',
145 * interval: '1 minute 30 seconds'
146 * })
147 * .on('health', (serviceHealth, stop) => {
148 * // Do something with the result
149 * // [{ name: String, health: Boolean, level: HARD/SOFT, lastCheck: Date }]
150 * // stop polling
151 * stop()
152 * })
153 * @example
154 * copacetic.poll({ name: 'my-dependency' })
155 * .on('health', () => { ... Do something })
156 * @fires Copacetic#health
157 * @param {String} [name] The identifier of a single dependency to be checked
158 * @param {Array<Object>} [dependencies] An explicit set of dependencies to be polled
159 * @param {String} [interval='5 seconds']
160 * @param {Boolean} [parallel=true] Kick of health checks in parallel or series
161 * @param {String} [schedule='start'] Schedule the next check to start (interval - ms) | ms
162 * @return {Copacetic}
163 */
164 poll ({
165 name,
166 dependencies,
167 interval = '5 seconds',
168 maxDelay = '30 seconds',
169 parallel = true,
170 schedule = 'start'
171 } = {}) {
172 const intervalMs = humanInterval(interval)
173 const start = process.hrtime()
174 const _check = name
175 ? () => this._checkOne(name, 1, humanInterval(maxDelay))
176 : (_dependencies) => this._checkMany(_dependencies, parallel)
177
178 const loop = () => {
179 let _dependencies = []
180
181 if (dependencies === 'all') {
182 _dependencies = this.dependencyNames.map(s => ({
183 name: this.getDependency(s).name,
184 retries: 1
185 }))
186 } else {
187 _dependencies = dependencies
188 }
189
190 _check(_dependencies)
191 .catch(e => e)
192 .then((res) => {
193 this.emit('health', res, this.stop.bind(this))
194
195 const delay = schedule === 'start'
196 ? intervalMs - (process.hrtime(start)[1] * 1e-6)
197 : intervalMs
198
199 if (this.isPolling !== false) {
200 this.pollTimeout = setTimeout(loop, delay)
201 } else {
202 this.emit('stopped')
203 }
204 })
205 }
206
207 this.isPolling = true
208 loop()
209
210 return this
211 }
212
213 /**
214 * stops polling registered dependencies
215 */
216 stop () {
217 this.isPolling = false
218 clearTimeout(this.pollTimeout)
219 }
220
221 /**
222 * Checks the health of all registered dependencies
223 * @param {Boolean} [parallel=true] Kick of health checks in parallel or series
224 * @return {Copacetic}
225 */
226 checkAll (parallel) {
227 const dependencies = this.dependencyNames.map(s => ({
228 name: this.getDependency(s).name,
229 retries: 1
230 }))
231
232 return this.check({ dependencies, parallel })
233 }
234
235 /**
236 * Checks the health of a set, or single dependency
237 * @example
238 * copacetic.check({ name: 'my-dependency' })
239 * @example
240 * copacetic.check({ name: 'my-dependency', retries: 5 })
241 * .on('healthy', serviceHealth => { ... Do stuff })
242 * .on('unhealthy', serviceHealth => { ... Handle degraded state })
243 * @example
244 * copacetic.check({ dependencies: [
245 * { name: 'my-dep' },
246 * { name: 'my-other-dep', retries: 2, maxDelay: '1 second' }
247 * ] })
248 * .on('health', (servicesHealth) => {
249 * // Do something with the result
250 * // [{ name: String, health: Boolean, level: HARD/SOFT, lastCheck: Date }]
251 * })
252 * @example
253 * copacetic.check({ name: 'my-dependency' })
254 * .then((health) => { ... Do Stuff })
255 * .catch((err) => { ... Handle degraded state })
256 * @fires Copacetic#health
257 * @fires Copacetic#healthy
258 * @fires Copacetic#unhealthy
259 * @param {String} [name] The identifier of a single dependency to be checked
260 * @param {Array<Object>} [dependencies] An explicit set of dependencies to be checked
261 * @param {Integer} [retries=1] How many times should a dependency be checked, until it
262 * is deemed unhealthy
263 * @param {Boolean} [parallel=true] Kick of health checks in parallel or series
264 * @return {Copacetic}
265 */
266 check ({ name, dependencies, retries = 1, maxDelay = '30 seconds', parallel = true } = {}) {
267 precond.checkIsNumber(retries, 'retries must be an integer')
268
269 const doCheck = name
270 ? () => this._checkOne(name, retries, humanInterval(maxDelay))
271 : () => this._checkMany(dependencies, parallel)
272
273 if (!this.eventEmitterMode) {
274 return doCheck()
275 }
276
277 if (name) {
278 doCheck()
279 /**
280 * Health information on a single dependency
281 *
282 * @event Copacetic#healthy
283 * @type {HealthInfo}
284 */
285 .then(s => this.emit('healthy', s))
286 /**
287 * Health information on a single dependency
288 *
289 * @event Copacetic#unhealthy
290 * @type {HealthInfo}
291 */
292 .catch(s => this.emit('unhealthy', s))
293 }
294
295 if (dependencies) {
296 doCheck()
297 /**
298 * Health information on a set of dependencies
299 *
300 * @event Copacetic#health
301 * @type {Array<HealthInfo>}
302 */
303 .then(s => this.emit('health', s))
304 }
305
306 return this
307 }
308
309 /**
310 * Convenience method that waits for a single, or set of dependencies
311 * to become healthy. Calling this means copacetic will keep re-checking
312 * indefinitely until the dependency(s) become healthy. If you want more
313 * control, use .check().
314 * @example
315 * // wait indefinitely
316 * copacetic.waitFor({ name: 'my-dependency'})
317 * .on('healthy', serviceHealth => { ... Do stuff })
318 * @example
319 * // give up after 5 tries
320 * copacetic.waitFor({ name: 'my-dependency', retries: 5})
321 * .on('healthy', serviceHealth => { ... Do stuff })
322 * @param {Object} opts - options accepted by check()
323 */
324 waitFor (opts = {}) {
325 const _opts = cloneDeep(opts)
326
327 if (opts.dependencies) {
328 _opts.dependencies = _opts.dependencies.map((d) => ({
329 name: d.name,
330 retries: 0,
331 maxDelay: d.maxDelay || 0
332 }))
333
334 return this.check(_opts)
335 }
336
337 _opts.retries = 0
338
339 return this.check(_opts)
340 }
341
342 /**
343 * @param {String} name The name used to identify a dependency
344 * @param {Integer} maxDelay The maximum interval of time to wait when retrying
345 * @return {Promise}
346 */
347 _checkOne (name, retries, maxDelay) {
348 precond.checkState(
349 this.isDependencyRegistered(name) === true,
350 `Tried to check dependency - ${name}, but dependency is not registered`
351 )
352
353 return this
354 .getDependency(name)
355 .check(retries, maxDelay)
356 }
357
358 /**
359 * Checks an array of dependencies in parallel or sequantially
360 * @param {Array<Dependency>} dependencies
361 * @param {Boolean} parallel
362 * @return {Promise}
363 */
364 _checkMany (dependencies, parallel) {
365 if (parallel) {
366 return promiseAllSilent(
367 dependencies.map(s => this._checkOne(s.name, s.retries, s.maxDelay))
368 )
369 }
370
371 return promiseSeriesSilent(
372 dependencies.map(s => this._checkOne(s.name, s.retries, s.maxDelay))
373 )
374 }
375 }
376
377 return (name, mode) => new Copacetic(name, mode)
378}