mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
feat: Handle database connection errors with 500 (#725)
* feat: Handle database connection errors with 500 - If database goes away while unleash is running, unleash now stays running, but all api endpoints will return 500. - This includes our health endpoint, which allows k8s or similar orchestrators to decide what should be done, rather than Unleash terminating unexpectedly
This commit is contained in:
parent
f49b5084eb
commit
8bf4214ddb
@ -23,12 +23,15 @@ class ClientMetricsDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeMetricsOlderThanOneHour() {
|
async removeMetricsOlderThanOneHour() {
|
||||||
const rows = await this.db(TABLE)
|
try {
|
||||||
.whereRaw("created_at < now() - interval '1 hour'")
|
const rows = await this.db(TABLE)
|
||||||
.del();
|
.whereRaw("created_at < now() - interval '1 hour'")
|
||||||
|
.del();
|
||||||
if (rows > 0) {
|
if (rows > 0) {
|
||||||
this.logger.debug(`Deleted ${rows} metrics`);
|
this.logger.debug(`Deleted ${rows} metrics`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Error when deleting metrics ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,26 +42,34 @@ class ClientMetricsDb {
|
|||||||
|
|
||||||
// Used at startup to load all metrics last week into memory!
|
// Used at startup to load all metrics last week into memory!
|
||||||
async getMetricsLastHour() {
|
async getMetricsLastHour() {
|
||||||
const result = await this.db
|
try {
|
||||||
.select(METRICS_COLUMNS)
|
const result = await this.db
|
||||||
.from(TABLE)
|
.select(METRICS_COLUMNS)
|
||||||
.limit(2000)
|
.from(TABLE)
|
||||||
.whereRaw("created_at > now() - interval '1 hour'")
|
.limit(2000)
|
||||||
.orderBy('created_at', 'asc');
|
.whereRaw("created_at > now() - interval '1 hour'")
|
||||||
|
.orderBy('created_at', 'asc');
|
||||||
return result.map(mapRow);
|
return result.map(mapRow);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`error when getting metrics last hour ${e}`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to poll for new metrics
|
// Used to poll for new metrics
|
||||||
async getNewMetrics(lastKnownId) {
|
async getNewMetrics(lastKnownId) {
|
||||||
const result = await this.db
|
try {
|
||||||
.select(METRICS_COLUMNS)
|
const res = await this.db
|
||||||
.from(TABLE)
|
.select(METRICS_COLUMNS)
|
||||||
.limit(1000)
|
.from(TABLE)
|
||||||
.where('id', '>', lastKnownId)
|
.limit(1000)
|
||||||
.orderBy('created_at', 'asc');
|
.where('id', '>', lastKnownId)
|
||||||
|
.orderBy('created_at', 'asc');
|
||||||
return result.map(mapRow);
|
return res.map(mapRow);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`error when getting new metrics ${e}`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -10,6 +10,7 @@ module.exports.createDb = function({ db, databaseSchema, getLogger }) {
|
|||||||
connection: db,
|
connection: db,
|
||||||
pool: db.pool,
|
pool: db.pool,
|
||||||
searchPath: databaseSchema,
|
searchPath: databaseSchema,
|
||||||
|
asyncStackTraces: true,
|
||||||
log: {
|
log: {
|
||||||
debug: msg => logger.debug(msg),
|
debug: msg => logger.debug(msg),
|
||||||
info: msg => logger.info(msg),
|
info: msg => logger.info(msg),
|
||||||
|
@ -13,48 +13,60 @@ const EVENT_COLUMNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
class EventStore extends EventEmitter {
|
class EventStore extends EventEmitter {
|
||||||
constructor(db) {
|
constructor(db, getLogger) {
|
||||||
super();
|
super();
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.logger = getLogger('lib/db/event-store.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async store(event) {
|
async store(event) {
|
||||||
await this.db('events').insert({
|
try {
|
||||||
type: event.type,
|
await this.db('events').insert({
|
||||||
created_by: event.createdBy, // eslint-disable-line
|
type: event.type,
|
||||||
data: event.data,
|
created_by: event.createdBy, // eslint-disable-line
|
||||||
tags: event.tags ? JSON.stringify(event.tags) : [],
|
data: event.data,
|
||||||
});
|
tags: event.tags ? JSON.stringify(event.tags) : [],
|
||||||
process.nextTick(() => this.emit(event.type, event));
|
});
|
||||||
|
process.nextTick(() => this.emit(event.type, event));
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Failed to store event ${e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvents() {
|
async getEvents() {
|
||||||
const rows = await this.db
|
try {
|
||||||
.select(EVENT_COLUMNS)
|
const rows = await this.db
|
||||||
.from('events')
|
.select(EVENT_COLUMNS)
|
||||||
.limit(100)
|
.from('events')
|
||||||
.orderBy('created_at', 'desc');
|
.limit(100)
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
return rows.map(this.rowToEvent);
|
return rows.map(this.rowToEvent);
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventsFilterByName(name) {
|
async getEventsFilterByName(name) {
|
||||||
const rows = await this.db
|
try {
|
||||||
.select(EVENT_COLUMNS)
|
const rows = await this.db
|
||||||
.from('events')
|
.select(EVENT_COLUMNS)
|
||||||
.limit(100)
|
.from('events')
|
||||||
.whereRaw("data ->> 'name' = ?", [name])
|
.limit(100)
|
||||||
.andWhere(
|
.whereRaw("data ->> 'name' = ?", [name])
|
||||||
'id',
|
.andWhere(
|
||||||
'>=',
|
'id',
|
||||||
this.db
|
'>=',
|
||||||
.select(this.db.raw('coalesce(max(id),0) as id'))
|
this.db
|
||||||
.from('events')
|
.select(this.db.raw('coalesce(max(id),0) as id'))
|
||||||
.where({ type: DROP_FEATURES }),
|
.from('events')
|
||||||
)
|
.where({ type: DROP_FEATURES }),
|
||||||
.orderBy('created_at', 'desc');
|
)
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
return rows.map(this.rowToEvent);
|
return rows.map(this.rowToEvent);
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rowToEvent(row) {
|
rowToEvent(row) {
|
||||||
|
@ -57,7 +57,13 @@ class MetricsMonitor {
|
|||||||
|
|
||||||
async function collectFeatureToggleMetrics() {
|
async function collectFeatureToggleMetrics() {
|
||||||
featureTogglesTotal.reset();
|
featureTogglesTotal.reset();
|
||||||
const togglesCount = await featureToggleStore.count();
|
let togglesCount;
|
||||||
|
try {
|
||||||
|
togglesCount = await featureToggleStore.count();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
togglesCount = togglesCount || 0;
|
||||||
featureTogglesTotal.labels(version).set(togglesCount);
|
featureTogglesTotal.labels(version).set(togglesCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,14 +111,14 @@ class MetricsMonitor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.configureDbMetrics(stores, eventStore);
|
this.configureDbMetrics(stores, eventBus);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopMonitoring() {
|
stopMonitoring() {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
configureDbMetrics(stores, eventStore) {
|
configureDbMetrics(stores, eventBus) {
|
||||||
if (stores.db && stores.db.client) {
|
if (stores.db && stores.db.client) {
|
||||||
const dbPoolMin = new client.Gauge({
|
const dbPoolMin = new client.Gauge({
|
||||||
name: 'db_pool_min',
|
name: 'db_pool_min',
|
||||||
@ -143,29 +149,31 @@ class MetricsMonitor {
|
|||||||
'how many acquires are waiting for a resource to be released in DB pool',
|
'how many acquires are waiting for a resource to be released in DB pool',
|
||||||
});
|
});
|
||||||
|
|
||||||
eventStore.on(DB_POOL_UPDATE, data => {
|
eventBus.on(DB_POOL_UPDATE, data => {
|
||||||
dbPoolFree.set(data.free);
|
dbPoolFree.set(data.free);
|
||||||
dbPoolUsed.set(data.used);
|
dbPoolUsed.set(data.used);
|
||||||
dbPoolPendingCreates.set(data.pendingCreates);
|
dbPoolPendingCreates.set(data.pendingCreates);
|
||||||
dbPoolPendingAcquires.set(data.pendingAcquires);
|
dbPoolPendingAcquires.set(data.pendingAcquires);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerPoolMetrics(stores.db.client.pool, eventStore);
|
this.registerPoolMetrics(stores.db.client.pool, eventBus);
|
||||||
setInterval(
|
setInterval(
|
||||||
() =>
|
() => this.registerPoolMetrics(stores.db.client.pool, eventBus),
|
||||||
this.registerPoolMetrics(stores.db.client.pool, eventStore),
|
|
||||||
ONE_MINUTE,
|
ONE_MINUTE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPoolMetrics(pool, eventStore) {
|
registerPoolMetrics(pool, eventBus) {
|
||||||
eventStore.emit(DB_POOL_UPDATE, {
|
try {
|
||||||
used: pool.numUsed(),
|
eventBus.emit(DB_POOL_UPDATE, {
|
||||||
free: pool.numFree(),
|
used: pool.numUsed(),
|
||||||
pendingCreates: pool.numPendingCreates(),
|
free: pool.numFree(),
|
||||||
pendingAcquires: pool.numPendingAcquires(),
|
pendingCreates: pool.numPendingCreates(),
|
||||||
});
|
pendingAcquires: pool.numPendingAcquires(),
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const monitor = createMetricsMonitor();
|
|||||||
|
|
||||||
test.before(() => {
|
test.before(() => {
|
||||||
const featureToggleStore = {
|
const featureToggleStore = {
|
||||||
count: () => 123,
|
count: async () => 123,
|
||||||
};
|
};
|
||||||
const config = {
|
const config = {
|
||||||
serverMetrics: true,
|
serverMetrics: true,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { handleErrors } from './util';
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
const extractUser = require('../../extract-user');
|
const extractUser = require('../../extract-user');
|
||||||
@ -16,8 +18,12 @@ class ArchiveController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getArchivedFeatures(req, res) {
|
async getArchivedFeatures(req, res) {
|
||||||
const features = await this.featureService.getArchivedFeatures();
|
try {
|
||||||
res.json({ features });
|
const features = await this.featureService.getArchivedFeatures();
|
||||||
|
res.json({ features });
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, this.logger, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reviveFeatureToggle(req, res) {
|
async reviveFeatureToggle(req, res) {
|
||||||
|
@ -34,10 +34,14 @@ class ContextController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getContextFields(req, res) {
|
async getContextFields(req, res) {
|
||||||
const fields = await this.contextService.getAll();
|
try {
|
||||||
res.status(200)
|
const fields = await this.contextService.getAll();
|
||||||
.json(fields)
|
res.status(200)
|
||||||
.end();
|
.json(fields)
|
||||||
|
.end();
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContextField(req, res) {
|
async getContextField(req, res) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { handleErrors } from './util';
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
const eventDiffer = require('../../event-differ');
|
const eventDiffer = require('../../event-differ');
|
||||||
@ -15,20 +17,30 @@ class EventController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEvents(req, res) {
|
async getEvents(req, res) {
|
||||||
const events = await this.eventStore.getEvents();
|
try {
|
||||||
eventDiffer.addDiffs(events);
|
const events = await this.eventStore.getEvents();
|
||||||
res.json({ version, events });
|
eventDiffer.addDiffs(events);
|
||||||
|
res.json({ version, events });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventsForToggle(req, res) {
|
async getEventsForToggle(req, res) {
|
||||||
const toggleName = req.params.name;
|
const toggleName = req.params.name;
|
||||||
const events = await this.eventStore.getEventsFilterByName(toggleName);
|
try {
|
||||||
|
const events = await this.eventStore.getEventsFilterByName(
|
||||||
|
toggleName,
|
||||||
|
);
|
||||||
|
|
||||||
if (events) {
|
if (events) {
|
||||||
eventDiffer.addDiffs(events);
|
eventDiffer.addDiffs(events);
|
||||||
res.json({ toggleName, events });
|
res.json({ toggleName, events });
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: 'Could not find events' });
|
res.status(404).json({ error: 'Could not find events' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { handleErrors } from './util';
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
@ -14,8 +16,12 @@ class FeatureTypeController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllFeatureTypes(req, res) {
|
async getAllFeatureTypes(req, res) {
|
||||||
const types = await this.featureTypeStore.getAll();
|
try {
|
||||||
res.json({ version, types });
|
const types = await this.featureTypeStore.getAll();
|
||||||
|
res.json({ version, types });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,11 +49,15 @@ class FeatureController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllToggles(req, res) {
|
async getAllToggles(req, res) {
|
||||||
const features = await this.featureService.getFeatures(
|
try {
|
||||||
req.query,
|
const features = await this.featureService.getFeatures(
|
||||||
fields,
|
req.query,
|
||||||
);
|
fields,
|
||||||
res.json({ version, features });
|
);
|
||||||
|
res.json({ version, features });
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, this.logger, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToggle(req, res) {
|
async getToggle(req, res) {
|
||||||
@ -67,8 +71,14 @@ class FeatureController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listTags(req, res) {
|
async listTags(req, res) {
|
||||||
const tags = await this.featureService.listTags(req.params.featureName);
|
try {
|
||||||
res.json({ version, tags });
|
const tags = await this.featureService.listTags(
|
||||||
|
req.params.featureName,
|
||||||
|
);
|
||||||
|
res.json({ version, tags });
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, this.logger, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTag(req, res) {
|
async addTag(req, res) {
|
||||||
@ -89,12 +99,16 @@ class FeatureController extends Controller {
|
|||||||
async removeTag(req, res) {
|
async removeTag(req, res) {
|
||||||
const { featureName, type, value } = req.params;
|
const { featureName, type, value } = req.params;
|
||||||
const userName = extractUser(req);
|
const userName = extractUser(req);
|
||||||
await this.featureService.removeTag(
|
try {
|
||||||
featureName,
|
await this.featureService.removeTag(
|
||||||
{ type, value },
|
featureName,
|
||||||
userName,
|
{ type, value },
|
||||||
);
|
userName,
|
||||||
res.status(200).end();
|
);
|
||||||
|
res.status(200).end();
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, this.logger, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(req, res) {
|
async validate(req, res) {
|
||||||
|
@ -16,9 +16,9 @@ const {
|
|||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup(databaseIsUp = true) {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const stores = store.createStores();
|
const stores = store.createStores(databaseIsUp);
|
||||||
const perms = permissions();
|
const perms = permissions();
|
||||||
const config = {
|
const config = {
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
@ -614,3 +614,9 @@ test('Tags should be included in updated events', async t => {
|
|||||||
t.is(events[0].tags[0].type, 'simple');
|
t.is(events[0].tags[0].type, 'simple');
|
||||||
t.is(events[0].tags[0].value, 'tag');
|
t.is(events[0].tags[0].value, 'tag');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Trying to get features while database is down should yield 500', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, base } = getSetup(false);
|
||||||
|
return request.get(`${base}/api/admin/features`).expect(500);
|
||||||
|
});
|
||||||
|
@ -28,29 +28,45 @@ class MetricsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSeenToggles(req, res) {
|
async getSeenToggles(req, res) {
|
||||||
const seenAppToggles = await this.metrics.getAppsWithToggles();
|
try {
|
||||||
res.json(seenAppToggles);
|
const seenAppToggles = await this.metrics.getAppsWithToggles();
|
||||||
|
res.json(seenAppToggles);
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSeenApps(req, res) {
|
async getSeenApps(req, res) {
|
||||||
const seenApps = await this.metrics.getSeenApps();
|
try {
|
||||||
res.json(seenApps);
|
const seenApps = await this.metrics.getSeenApps();
|
||||||
|
res.json(seenApps);
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureToggles(req, res) {
|
async getFeatureToggles(req, res) {
|
||||||
const toggles = await this.metrics.getTogglesMetrics();
|
try {
|
||||||
res.json(toggles);
|
const toggles = await this.metrics.getTogglesMetrics();
|
||||||
|
res.json(toggles);
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureToggle(req, res) {
|
async getFeatureToggle(req, res) {
|
||||||
const { name } = req.params;
|
try {
|
||||||
const data = await this.metrics.getTogglesMetrics();
|
const { name } = req.params;
|
||||||
const lastHour = data.lastHour[name] || {};
|
const data = await this.metrics.getTogglesMetrics();
|
||||||
const lastMinute = data.lastMinute[name] || {};
|
const lastHour = data.lastHour[name] || {};
|
||||||
res.json({
|
const lastMinute = data.lastMinute[name] || {};
|
||||||
lastHour,
|
res.json({
|
||||||
lastMinute,
|
lastHour,
|
||||||
});
|
lastMinute,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteApplication(req, res) {
|
async deleteApplication(req, res) {
|
||||||
|
@ -36,8 +36,12 @@ class StrategyController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllStratgies(req, res) {
|
async getAllStratgies(req, res) {
|
||||||
const strategies = await this.strategyService.getStrategies();
|
try {
|
||||||
res.json({ version, strategies });
|
const strategies = await this.strategyService.getStrategies();
|
||||||
|
res.json({ version, strategies });
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, this.logger, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStrategy(req, res) {
|
async getStrategy(req, res) {
|
||||||
|
@ -16,10 +16,10 @@ const {
|
|||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup(databaseIsUp = true) {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const perms = permissions();
|
const perms = permissions();
|
||||||
const stores = store.createStores();
|
const stores = store.createStores(databaseIsUp);
|
||||||
const config = {
|
const config = {
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
stores,
|
stores,
|
||||||
@ -261,3 +261,9 @@ test(`deprecating 'default' strategy will yield 403`, t => {
|
|||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(403);
|
.expect(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Getting strategies while database is down should yield 500', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, base } = getSetup(false);
|
||||||
|
return request.get(`${base}/api/admin/strategies`).expect(500);
|
||||||
|
});
|
||||||
|
@ -22,8 +22,12 @@ class TagTypeController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTagTypes(req, res) {
|
async getTagTypes(req, res) {
|
||||||
const tagTypes = await this.tagTypeService.getAll();
|
try {
|
||||||
res.json({ version, tagTypes });
|
const tagTypes = await this.tagTypeService.getAll();
|
||||||
|
res.json({ version, tagTypes });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(req, res) {
|
async validate(req, res) {
|
||||||
|
@ -22,13 +22,21 @@ class TagController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTags(req, res) {
|
async getTags(req, res) {
|
||||||
const tags = await this.tagService.getTags();
|
try {
|
||||||
res.json({ version, tags });
|
const tags = await this.tagService.getTags();
|
||||||
|
res.json({ version, tags });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTagsByType(req, res) {
|
async getTagsByType(req, res) {
|
||||||
const tags = await this.tagService.getTagsByType(req.params.type);
|
try {
|
||||||
res.json({ version, tags });
|
const tags = await this.tagService.getTagsByType(req.params.type);
|
||||||
|
res.json({ version, tags });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTag(req, res) {
|
async getTag(req, res) {
|
||||||
|
@ -12,9 +12,9 @@ const { UPDATE_FEATURE } = require('../../permissions');
|
|||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup(databaseIsUp = true) {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const stores = store.createStores();
|
const stores = store.createStores(databaseIsUp);
|
||||||
const perms = permissions();
|
const perms = permissions();
|
||||||
const config = {
|
const config = {
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
@ -118,3 +118,9 @@ test('should be able to filter by type', t => {
|
|||||||
t.is(res.body.tags[0].value, 'TeamRed');
|
t.is(res.body.tags[0].value, 'TeamRed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Getting tags while database is down should be a 500', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, base } = getSetup(false);
|
||||||
|
return request.get(`${base}/api/admin/tags`).expect(500);
|
||||||
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { handleErrors } from '../admin-api/util';
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
@ -23,11 +25,15 @@ class FeatureController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(req, res) {
|
async getAll(req, res) {
|
||||||
const features = await this.toggleService.getFeatures(
|
try {
|
||||||
req.query,
|
const features = await this.toggleService.getFeatures(
|
||||||
FEATURE_COLUMNS_CLIENT,
|
req.query,
|
||||||
);
|
FEATURE_COLUMNS_CLIENT,
|
||||||
res.json({ version, features });
|
);
|
||||||
|
res.json({ version, features });
|
||||||
|
} catch (e) {
|
||||||
|
handleErrors(res, this.logger, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureToggle(req, res) {
|
async getFeatureToggle(req, res) {
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (databaseIsUp = true) => {
|
||||||
const _features = [];
|
const _features = [];
|
||||||
const _archive = [];
|
const _archive = [];
|
||||||
const _featureTags = {};
|
const _featureTags = {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getFeature: name => {
|
getFeature: name => {
|
||||||
|
if (!databaseIsUp) {
|
||||||
|
return Promise.reject(new Error('No database connection'));
|
||||||
|
}
|
||||||
const toggle = _features.find(f => f.name === name);
|
const toggle = _features.find(f => f.name === name);
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
return Promise.resolve(toggle);
|
return Promise.resolve(toggle);
|
||||||
@ -63,6 +66,9 @@ module.exports = () => {
|
|||||||
},
|
},
|
||||||
importFeature: feat => Promise.resolve(_features.push(feat)),
|
importFeature: feat => Promise.resolve(_features.push(feat)),
|
||||||
getFeatures: query => {
|
getFeatures: query => {
|
||||||
|
if (!databaseIsUp) {
|
||||||
|
return Promise.reject(new Error('No database connection'));
|
||||||
|
}
|
||||||
if (query) {
|
if (query) {
|
||||||
const activeQueryKeys = Object.keys(query).filter(
|
const activeQueryKeys = Object.keys(query).filter(
|
||||||
t => query[t],
|
t => query[t],
|
||||||
|
9
src/test/fixtures/fake-strategies-store.js
vendored
9
src/test/fixtures/fake-strategies-store.js
vendored
@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
const NotFoundError = require('../../lib/error/notfound-error');
|
const NotFoundError = require('../../lib/error/notfound-error');
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (databaseIsUp = true) => {
|
||||||
const _strategies = [
|
const _strategies = [
|
||||||
{ name: 'default', editable: false, parameters: {}, deprecated: false },
|
{ name: 'default', editable: false, parameters: {}, deprecated: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStrategies: () => Promise.resolve(_strategies),
|
getStrategies: () => {
|
||||||
|
if (databaseIsUp) {
|
||||||
|
return Promise.resolve(_strategies);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('No database connection'));
|
||||||
|
},
|
||||||
getEditableStrategies: () =>
|
getEditableStrategies: () =>
|
||||||
Promise.resolve(_strategies.filter(s => s.editable)),
|
Promise.resolve(_strategies.filter(s => s.editable)),
|
||||||
getStrategy: name => {
|
getStrategy: name => {
|
||||||
|
12
src/test/fixtures/fake-tag-store.js
vendored
12
src/test/fixtures/fake-tag-store.js
vendored
@ -1,9 +1,12 @@
|
|||||||
const NotFoundError = require('../../lib/error/notfound-error');
|
const NotFoundError = require('../../lib/error/notfound-error');
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (databaseIsUp = true) => {
|
||||||
const _tags = [];
|
const _tags = [];
|
||||||
return {
|
return {
|
||||||
getTagsByType: type => {
|
getTagsByType: type => {
|
||||||
|
if (!databaseIsUp) {
|
||||||
|
return Promise.reject(new Error('No database connection'));
|
||||||
|
}
|
||||||
const tags = _tags.filter(t => t.type === type);
|
const tags = _tags.filter(t => t.type === type);
|
||||||
return Promise.resolve(tags);
|
return Promise.resolve(tags);
|
||||||
},
|
},
|
||||||
@ -18,7 +21,12 @@ module.exports = () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAll: () => Promise.resolve(_tags),
|
getAll: () => {
|
||||||
|
if (!databaseIsUp) {
|
||||||
|
return Promise.reject(new Error('No database connection'));
|
||||||
|
}
|
||||||
|
return Promise.resolve(_tags);
|
||||||
|
},
|
||||||
getTag: (type, value) => {
|
getTag: (type, value) => {
|
||||||
const tag = _tags.find(t => t.type === type && t.value === value);
|
const tag = _tags.find(t => t.type === type && t.value === value);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
|
24
src/test/fixtures/store.js
vendored
24
src/test/fixtures/store.js
vendored
@ -13,7 +13,7 @@ const settingStore = require('./fake-setting-store');
|
|||||||
const addonStore = require('./fake-addon-store');
|
const addonStore = require('./fake-addon-store');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createStores: () => {
|
createStores: (databaseIsUp = true) => {
|
||||||
const db = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
from: () => Promise.resolve(),
|
from: () => Promise.resolve(),
|
||||||
@ -22,17 +22,17 @@ module.exports = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
clientApplicationsStore: clientApplicationsStore(),
|
clientApplicationsStore: clientApplicationsStore(databaseIsUp),
|
||||||
clientMetricsStore: new ClientMetricsStore(),
|
clientMetricsStore: new ClientMetricsStore(databaseIsUp),
|
||||||
clientInstanceStore: clientInstanceStore(),
|
clientInstanceStore: clientInstanceStore(databaseIsUp),
|
||||||
featureToggleStore: featureToggleStore(),
|
featureToggleStore: featureToggleStore(databaseIsUp),
|
||||||
tagStore: tagStore(),
|
tagStore: tagStore(databaseIsUp),
|
||||||
tagTypeStore: tagTypeStore(),
|
tagTypeStore: tagTypeStore(databaseIsUp),
|
||||||
eventStore: new EventStore(),
|
eventStore: new EventStore(databaseIsUp),
|
||||||
strategyStore: strategyStore(),
|
strategyStore: strategyStore(databaseIsUp),
|
||||||
contextFieldStore: contextFieldStore(),
|
contextFieldStore: contextFieldStore(databaseIsUp),
|
||||||
settingStore: settingStore(),
|
settingStore: settingStore(databaseIsUp),
|
||||||
addonStore: addonStore(),
|
addonStore: addonStore(databaseIsUp),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user