1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: add stop() method to gracefully terminate unleash (#665)

This commit is contained in:
Laurent Dezitter 2020-12-16 08:49:11 -05:00 committed by GitHub
parent db51104198
commit 5857f0e58d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 182 additions and 86 deletions

View File

@ -38,7 +38,7 @@ class ClientInstanceStore {
}); });
const clearer = () => this._removeInstancesOlderThanTwoDays(); const clearer = () => this._removeInstancesOlderThanTwoDays();
setTimeout(clearer, 10).unref(); setTimeout(clearer, 10).unref();
setInterval(clearer, ONE_DAY).unref(); this.timer = setInterval(clearer, ONE_DAY).unref();
} }
async _removeInstancesOlderThanTwoDays() { async _removeInstancesOlderThanTwoDays() {
@ -133,6 +133,10 @@ class ClientInstanceStore {
.where('app_name', appName) .where('app_name', appName)
.del(); .del();
} }
destroy() {
clearInterval(this.timer);
}
} }
module.exports = ClientInstanceStore; module.exports = ClientInstanceStore;

View File

@ -19,7 +19,7 @@ class ClientMetricsDb {
// Clear old metrics regulary // Clear old metrics regulary
const clearer = () => this.removeMetricsOlderThanOneHour(); const clearer = () => this.removeMetricsOlderThanOneHour();
setTimeout(clearer, 10).unref(); setTimeout(clearer, 10).unref();
setInterval(clearer, ONE_MINUTE).unref(); this.timer = setInterval(clearer, ONE_MINUTE).unref();
} }
async removeMetricsOlderThanOneHour() { async removeMetricsOlderThanOneHour() {
@ -60,6 +60,10 @@ class ClientMetricsDb {
return result.map(mapRow); return result.map(mapRow);
} }
destroy() {
clearInterval(this.timer);
}
} }
module.exports = ClientMetricsDb; module.exports = ClientMetricsDb;

View File

@ -62,11 +62,8 @@ class ClientMetricsStore extends EventEmitter {
} }
destroy() { destroy() {
try {
clearInterval(this.timer); clearInterval(this.timer);
} catch (e) { this.metricsDb.destroy();
// empty
}
} }
} }

View File

@ -20,6 +20,9 @@ function getMockDb() {
getNewMetrics() { getNewMetrics() {
return Promise.resolve([list.pop() || { id: 0 }]); return Promise.resolve([list.pop() || { id: 0 }]);
}, },
destroy() {
// noop
},
}; };
} }

View File

@ -11,7 +11,12 @@ const {
const THREE_HOURS = 3 * 60 * 60 * 1000; const THREE_HOURS = 3 * 60 * 60 * 1000;
exports.startMonitoring = ({ serverMetrics, eventBus, stores, version }) => { class MetricsMonitor {
constructor() {
this.timer = null;
}
startMonitoring({ serverMetrics, eventBus, stores, version }) {
if (!serverMetrics) { if (!serverMetrics) {
return; return;
} }
@ -55,11 +60,17 @@ exports.startMonitoring = ({ serverMetrics, eventBus, stores, version }) => {
} }
collectFeatureToggleMetrics(); collectFeatureToggleMetrics();
setInterval(() => collectFeatureToggleMetrics(), THREE_HOURS); this.timer = setInterval(
() => collectFeatureToggleMetrics(),
THREE_HOURS,
).unref();
eventBus.on(events.REQUEST_TIME, ({ path, method, time, statusCode }) => { eventBus.on(
events.REQUEST_TIME,
({ path, method, time, statusCode }) => {
requestDuration.labels(path, method, statusCode).observe(time); requestDuration.labels(path, method, statusCode).observe(time);
}); },
);
eventBus.on(events.DB_TIME, ({ store, action, time }) => { eventBus.on(events.DB_TIME, ({ store, action, time }) => {
dbDuration.labels(store, action).observe(time); dbDuration.labels(store, action).observe(time);
@ -80,9 +91,26 @@ exports.startMonitoring = ({ serverMetrics, eventBus, stores, version }) => {
clientMetricsStore.on('metrics', m => { clientMetricsStore.on('metrics', m => {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const [feature, { yes, no }] of Object.entries(m.bucket.toggles)) { for (const [feature, { yes, no }] of Object.entries(
featureToggleUsageTotal.labels(feature, true, m.appName).inc(yes); m.bucket.toggles,
featureToggleUsageTotal.labels(feature, false, m.appName).inc(no); )) {
featureToggleUsageTotal
.labels(feature, true, m.appName)
.inc(yes);
featureToggleUsageTotal
.labels(feature, false, m.appName)
.inc(no);
} }
}); });
}
stopMonitoring() {
clearInterval(this.timer);
}
}
module.exports = {
createMetricsMonitor() {
return new MetricsMonitor();
},
}; };

View File

@ -9,7 +9,9 @@ const clientMetricsStore = new EventEmitter();
const { register: prometheusRegister } = require('prom-client'); const { register: prometheusRegister } = require('prom-client');
const { REQUEST_TIME, DB_TIME } = require('./events'); const { REQUEST_TIME, DB_TIME } = require('./events');
const { FEATURE_UPDATED } = require('./event-type'); const { FEATURE_UPDATED } = require('./event-type');
const { startMonitoring } = require('./metrics'); const { createMetricsMonitor } = require('./metrics');
const monitor = createMetricsMonitor();
test.before(() => { test.before(() => {
const featureToggleStore = { const featureToggleStore = {
@ -25,7 +27,11 @@ test.before(() => {
}, },
version: '3.4.1', version: '3.4.1',
}; };
startMonitoring(config); monitor.startMonitoring(config);
});
test.after(() => {
monitor.stopMonitoring();
}); });
test('should collect metrics for requests', t => { test('should collect metrics for requests', t => {

View File

@ -5,7 +5,7 @@ const { EventEmitter } = require('events');
const migrator = require('../migrator'); const migrator = require('../migrator');
const getApp = require('./app'); const getApp = require('./app');
const { startMonitoring } = require('./metrics'); const { createMetricsMonitor } = require('./metrics');
const { createStores } = require('./db'); const { createStores } = require('./db');
const { createServices } = require('./services'); const { createServices } = require('./services');
const { createOptions } = require('./options'); const { createOptions } = require('./options');
@ -15,6 +15,27 @@ const AuthenticationRequired = require('./authentication-required');
const { addEventHook } = require('./event-hook'); const { addEventHook } = require('./event-hook');
const eventType = require('./event-type'); const eventType = require('./event-type');
async function closeServer(opts) {
const { server, metricsMonitor } = opts;
metricsMonitor.stopMonitoring();
return new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
}
async function destroyDatabase(stores) {
const { db, clientInstanceStore, clientMetricsStore } = stores;
return new Promise((resolve, reject) => {
clientInstanceStore.destroy();
clientMetricsStore.destroy();
db.destroy(error => (error ? reject(error) : resolve()));
});
}
async function createApp(options) { async function createApp(options) {
// Database dependencies (stateful) // Database dependencies (stateful)
const logger = options.getLogger('server-impl.js'); const logger = options.getLogger('server-impl.js');
@ -33,7 +54,8 @@ async function createApp(options) {
}; };
const app = getApp(config); const app = getApp(config);
startMonitoring(config); const metricsMonitor = createMetricsMonitor();
metricsMonitor.startMonitoring(config);
if (typeof config.eventHook === 'function') { if (typeof config.eventHook === 'function') {
addEventHook(config.eventHook, stores.eventStore); addEventHook(config.eventHook, stores.eventStore);
@ -61,14 +83,28 @@ async function createApp(options) {
const server = app.listen(options.listen, () => const server = app.listen(options.listen, () =>
logger.info('Unleash has started.', server.address()), logger.info('Unleash has started.', server.address()),
); );
const stop = () => {
logger.info('Shutting down Unleash...');
return closeServer({ server, metricsMonitor }).then(() => {
return destroyDatabase(stores);
});
};
server.keepAliveTimeout = options.keepAliveTimeout; server.keepAliveTimeout = options.keepAliveTimeout;
server.headersTimeout = options.headersTimeout; server.headersTimeout = options.headersTimeout;
server.on('listening', () => { server.on('listening', () => {
resolve({ ...payload, server }); resolve({ ...payload, server, stop });
}); });
server.on('error', reject); server.on('error', reject);
} else { } else {
resolve({ ...payload }); const stop = () => {
logger.info('Shutting down Unleash...');
metricsMonitor.stopMonitoring();
return destroyDatabase(stores);
};
resolve({ ...payload, stop });
} }
}); });
} }

View File

@ -14,6 +14,8 @@ const getApp = proxyquire('./app', {
}, },
}); });
const noop = () => {};
const eventStore = new EventEmitter(); const eventStore = new EventEmitter();
const settingStore = { const settingStore = {
get: () => { get: () => {
@ -24,13 +26,19 @@ const settingStore = {
const serverImpl = proxyquire('./server-impl', { const serverImpl = proxyquire('./server-impl', {
'./app': getApp, './app': getApp,
'./metrics': { './metrics': {
startMonitoring(o) { createMetricsMonitor() {
return o; return {
startMonitoring: noop,
stopMonitoring: noop,
};
}, },
}, },
'./db': { './db': {
createStores() { createStores() {
return { return {
db: { destroy: cb => cb() },
clientInstanceStore: { destroy: noop },
clientMetricsStore: { destroy: noop },
eventStore, eventStore,
settingStore, settingStore,
}; };
@ -99,3 +107,13 @@ test('should not create a server using create()', async t => {
}); });
t.true(typeof server === 'undefined'); t.true(typeof server === 'undefined');
}); });
test.only('should shutdown the server when calling stop()', async t => {
const { server, stop } = await serverImpl.start({
port: 0,
getLogger,
start: true,
});
await stop();
t.is(server.address(), null);
});