1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Added explicit pool settings in options.db object

- Also adds metrics for min and max pool size
- Metrics for free/used connections.
- Metrics for pending and current acquires

fixes: #705
This commit is contained in:
Christopher Kolstad 2021-02-04 14:14:46 +01:00
parent 844fb5974e
commit e952ae20a8
No known key found for this signature in database
GPG Key ID: 559ACB0E3DB5538A
9 changed files with 171 additions and 11 deletions

View File

@ -21,6 +21,9 @@
- docs: update getting started guide with docker options (#697) - docs: update getting started guide with docker options (#697)
- fix typo in /api/client/features docs (#694) - fix typo in /api/client/features docs (#694)
- fix: website: require immer 8.0.1 or higher - fix: website: require immer 8.0.1 or higher
- fix: Add support for configuring database pool size (#705)
- feat: Set default min dbpool size to 0
- feat: Set default max dbpool size to 4
## 3.10.1 ## 3.10.1

View File

@ -16,6 +16,11 @@ const unleashOptions = {
port: 5432, port: 5432,
database: 'unleash', database: 'unleash',
ssl: false, ssl: false,
pool: {
min: 0,
max: 4,
idleTimeoutMillis: 30000,
},
}, },
enableRequestLogger: true, enableRequestLogger: true,
}; };
@ -33,6 +38,10 @@ unleash.start(unleashOptions);
- _database_ - the database name to be used (`DATABASE_NAME`) - _database_ - the database name to be used (`DATABASE_NAME`)
- _ssl_ - an object describing ssl options, see https://node-postgres.com/features/ssl (`DATABASE_SSL`, as a stringified json object) - _ssl_ - an object describing ssl options, see https://node-postgres.com/features/ssl (`DATABASE_SSL`, as a stringified json object)
- _version_ - the postgres database version. Used to connect a non-standard database. Defaults to `undefined`, which let the underlying adapter to detect the version automatically. (`DATABASE_VERSION`) - _version_ - the postgres database version. Used to connect a non-standard database. Defaults to `undefined`, which let the underlying adapter to detect the version automatically. (`DATABASE_VERSION`)
- _pool_ - an object describing pool options, see https://knexjs.org/#Installation-pooling. We support the following three fields:
- _min_ - minimum connections in connections pool (defaults to 0) (`DATABASE_POOL_MIN`)
- _max_ - maximum connections in connections pool (defaults to 4) (`DATABASE_POOL_MAX`)
- _idleTimeoutMillis_ - time in milliseconds a connection must be idle before being marked as a candidate for eviction (defaults to 30000) (`DATABASE_POOL_IDLE_TIMEOUT_MS`)
- **databaseUrl** - (_deprecated_) the postgres database url to connect to. Only used if _db_ object is not specified. Should include username/password. This value may also be set via the `DATABASE_URL` environment variable. Alternatively, if you would like to read the database url from a file, you may set the `DATABASE_URL_FILE` environment variable with the full file path. The contents of the file must be the database url exactly. - **databaseUrl** - (_deprecated_) the postgres database url to connect to. Only used if _db_ object is not specified. Should include username/password. This value may also be set via the `DATABASE_URL` environment variable. Alternatively, if you would like to read the database url from a file, you may set the `DATABASE_URL_FILE` environment variable with the full file path. The contents of the file must be the database url exactly.
- **databaseSchema** - the postgres database schema to use. Defaults to 'public'. (`DATABASE_SCHEMA`) - **databaseSchema** - the postgres database schema to use. Defaults to 'public'. (`DATABASE_SCHEMA`)
- **port** - which port the unleash-server should bind to. If port is omitted or is 0, the operating system will assign an arbitrary unused port. Will be ignored if pipe is specified. This value may also be set via the `HTTP_PORT` environment variable - **port** - which port the unleash-server should bind to. If port is omitted or is 0, the operating system will assign an arbitrary unused port. Will be ignored if pipe is specified. This value may also be set via the `HTTP_PORT` environment variable
@ -93,3 +102,8 @@ function getLogger(name) {
``` ```
The logger interface with its `debug`, `info`, `warn` and `error` methods expects format string support as seen in `debug` or the JavaScript `console` object. Many commonly used logging implementations cover this API, e.g., bunyan, pino or winston. The logger interface with its `debug`, `info`, `warn` and `error` methods expects format string support as seen in `debug` or the JavaScript `console` object. Many commonly used logging implementations cover this API, e.g., bunyan, pino or winston.
## Database pooling connection timeouts
- Please be aware of the default values of connection pool about idle session handling.
- If you have a network component which closes idle sessions on tcp layer, please ensure, that the connection pool idleTimeoutMillis setting is lower than the timespan as the network component will close the idle connection.

View File

@ -2,19 +2,13 @@
const knex = require('knex'); const knex = require('knex');
module.exports.createDb = function({ module.exports.createDb = function({ db, databaseSchema, getLogger }) {
db,
poolMin = 2,
poolMax = 20,
databaseSchema,
getLogger,
}) {
const logger = getLogger('db-pool.js'); const logger = getLogger('db-pool.js');
return knex({ return knex({
client: 'pg', client: 'pg',
version: db.version, version: db.version,
connection: db, connection: db,
pool: { min: poolMin, max: poolMax }, pool: db.pool,
searchPath: databaseSchema, searchPath: databaseSchema,
log: { log: {
debug: msg => logger.debug(msg), debug: msg => logger.debug(msg),

View File

@ -31,4 +31,5 @@ module.exports = {
ADDON_CONFIG_CREATED: 'addon-config-created', ADDON_CONFIG_CREATED: 'addon-config-created',
ADDON_CONFIG_UPDATED: 'addon-config-updated', ADDON_CONFIG_UPDATED: 'addon-config-updated',
ADDON_CONFIG_DELETED: 'addon-config-deleted', ADDON_CONFIG_DELETED: 'addon-config-deleted',
DB_POOL_UPDATE: 'db-pool-update',
}; };

View File

@ -7,9 +7,11 @@ const {
FEATURE_UPDATED, FEATURE_UPDATED,
FEATURE_ARCHIVED, FEATURE_ARCHIVED,
FEATURE_REVIVED, FEATURE_REVIVED,
DB_POOL_UPDATE,
} = require('./event-type'); } = require('./event-type');
const THREE_HOURS = 3 * 60 * 60 * 1000; const THREE_HOURS = 3 * 60 * 60 * 1000;
const ONE_MINUTE = 60 * 1000;
class MetricsMonitor { class MetricsMonitor {
constructor() { constructor() {
@ -102,11 +104,69 @@ class MetricsMonitor {
.inc(no); .inc(no);
} }
}); });
this.configureDbMetrics(stores, eventStore);
} }
stopMonitoring() { stopMonitoring() {
clearInterval(this.timer); clearInterval(this.timer);
} }
configureDbMetrics(stores, eventStore) {
if (stores.db && stores.db.client) {
const dbPoolMin = new client.Gauge({
name: 'db_pool_min',
help: 'Minimum DB pool size',
});
dbPoolMin.set(stores.db.client.pool.min);
const dbPoolMax = new client.Gauge({
name: 'db_pool_max',
help: 'Maximum DB pool size',
});
dbPoolMax.set(stores.db.client.pool.max);
const dbPoolFree = new client.Gauge({
name: 'db_pool_free',
help: 'Current free connections in DB pool',
});
const dbPoolUsed = new client.Gauge({
name: 'db_pool_used',
help: 'Current connections in use in DB pool',
});
const dbPoolPendingCreates = new client.Gauge({
name: 'db_pool_pending_creates',
help:
'how many asynchronous create calls are running in DB pool',
});
const dbPoolPendingAcquires = new client.Gauge({
name: 'db_pool_pending_acquires',
help:
'how many acquires are waiting for a resource to be released in DB pool',
});
eventStore.on(DB_POOL_UPDATE, data => {
dbPoolFree.set(data.free);
dbPoolUsed.set(data.used);
dbPoolPendingCreates.set(data.pendingCreates);
dbPoolPendingAcquires.set(data.pendingAcquires);
});
this.registerPoolMetrics(stores.db.client.pool, eventStore);
setInterval(
() =>
this.registerPoolMetrics(stores.db.client.pool, eventStore),
ONE_MINUTE,
);
}
}
registerPoolMetrics(pool, eventStore) {
eventStore.emit(DB_POOL_UPDATE, {
used: pool.numUsed(),
free: pool.numFree(),
pendingCreates: pool.numPendingCreates(),
pendingAcquires: pool.numPendingAcquires(),
});
}
} }
module.exports = { module.exports = {

View File

@ -24,6 +24,18 @@ test.before(() => {
eventStore, eventStore,
clientMetricsStore, clientMetricsStore,
featureToggleStore, featureToggleStore,
db: {
client: {
pool: {
min: 0,
max: 4,
numUsed: () => 2,
numFree: () => 2,
numPendingAcquires: () => 0,
numPendingCreates: () => 1,
},
},
},
}, },
version: '3.4.1', version: '3.4.1',
}; };
@ -95,3 +107,13 @@ test('should collect metrics for feature toggle size', async t => {
const metrics = await prometheusRegister.metrics(); const metrics = await prometheusRegister.metrics();
t.regex(metrics, /feature_toggles_total{version="(.*)"} 123/); t.regex(metrics, /feature_toggles_total{version="(.*)"} 123/);
}); });
test('Should collect metrics for database', async t => {
const metrics = await prometheusRegister.metrics();
t.regex(metrics, /db_pool_max/);
t.regex(metrics, /db_pool_min/);
t.regex(metrics, /db_pool_used/);
t.regex(metrics, /db_pool_free/);
t.regex(metrics, /db_pool_pending_creates/);
t.regex(metrics, /db_pool_pending_acquires/);
});

View File

@ -19,6 +19,18 @@ function defaultDatabaseUrl() {
return undefined; return undefined;
} }
function safeNumber(envVar, defaultVal) {
if (envVar) {
try {
return Number.parseInt(envVar, 10);
} catch (err) {
return defaultVal;
}
} else {
return defaultVal;
}
}
function defaultOptions() { function defaultOptions() {
return { return {
databaseUrl: defaultDatabaseUrl(), databaseUrl: defaultDatabaseUrl(),
@ -35,6 +47,14 @@ function defaultOptions() {
: false, : false,
driver: 'postgres', driver: 'postgres',
version: process.env.DATABASE_VERSION, version: process.env.DATABASE_VERSION,
pool: {
min: safeNumber(process.env.DATABASE_POOL_MIN, 0),
max: safeNumber(process.env.DATABASE_POOL_MAX, 4),
idleTimeoutMillis: safeNumber(
process.env.DATABASE_POOL_IDLE_TIMEOUT_MS,
30000,
),
},
}, },
port: process.env.HTTP_PORT || process.env.PORT || 4242, port: process.env.HTTP_PORT || process.env.PORT || 4242,
host: process.env.HTTP_HOST, host: process.env.HTTP_HOST,
@ -71,6 +91,15 @@ module.exports = {
// Use DATABASE_URL when 'db' not defined. // Use DATABASE_URL when 'db' not defined.
if (!opts.db && options.databaseUrl) { if (!opts.db && options.databaseUrl) {
options.db = parseDbUrl(options.databaseUrl); options.db = parseDbUrl(options.databaseUrl);
options.db.pool = defaultOptions().db.pool;
}
// If poolMin and poolMax is set, override pool settings
if (opts.poolMin) {
options.db.pool.min = opts.poolMin;
}
if (opts.poolMax) {
options.db.pool.max = opts.poolMax;
} }
if (!options.db.host) { if (!options.db.host) {

View File

@ -100,6 +100,11 @@ test('should expand databaseUrl from options', t => {
password: 'p', password: 'p',
port: '5432', port: '5432',
user: 'u', user: 'u',
pool: {
idleTimeoutMillis: 30000,
max: 4,
min: 0,
},
}); });
}); });
@ -136,6 +141,11 @@ test('should prefer custom db connection options', t => {
ssl: false, ssl: false,
driver: 'postgres', driver: 'postgres',
version: '10', version: '10',
pool: {
max: 4,
min: 0,
idleTimeoutMillis: 30000,
},
}; };
const options = createOptions({ databaseUrl, db }); const options = createOptions({ databaseUrl, db });
@ -149,3 +159,32 @@ test('should baseUriPath', t => {
t.deepEqual(options.baseUriPath, baseUriPath); t.deepEqual(options.baseUriPath, baseUriPath);
}); });
test('should allow setting pool size', t => {
const min = 4;
const max = 20;
const db = {
user: 'db_user',
password: 'db_password',
host: 'db-host',
port: 5432,
database: 'unleash',
pool: {
min,
max,
},
};
const options = createOptions({ db });
t.is(options.db.pool.min, min);
t.is(options.db.pool.max, max);
t.is(options.db.driver, 'postgres');
});
test('Should allow using outer poolMin and poolMax to set poolsize', t => {
const databaseUrl = 'postgres://u:p@localhost:5432/options';
const poolMin = 10;
const poolMax = 20;
const options = createOptions({ databaseUrl, poolMax, poolMin });
t.is(options.db.pool.min, 10);
t.is(options.db.pool.max, 20);
});

View File

@ -82,10 +82,8 @@ async function setupDatabase(stores) {
module.exports = async function init(databaseSchema = 'test', getLogger) { module.exports = async function init(databaseSchema = 'test', getLogger) {
const options = { const options = {
db: dbConfig.getDb(), db: { ...dbConfig.getDb(), pool: { min: 2, max: 8 } },
databaseSchema, databaseSchema,
minPool: 1,
maxPool: 1,
getLogger, getLogger,
}; };