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:
parent
db51104198
commit
5857f0e58d
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -62,11 +62,8 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
try {
|
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
} catch (e) {
|
this.metricsDb.destroy();
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ function getMockDb() {
|
|||||||
getNewMetrics() {
|
getNewMetrics() {
|
||||||
return Promise.resolve([list.pop() || { id: 0 }]);
|
return Promise.resolve([list.pop() || { id: 0 }]);
|
||||||
},
|
},
|
||||||
|
destroy() {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -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 => {
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user