lib/util/kvstore.js
'use strict';
const $err = require('./application_error');
const redis = require('redis');
/*!
* Reconnection strategy for redis
*/
function retryStrategy (ravelInstance) {
return function (options) {
const code = options.error ? options.error.code : 'Reason Unknown';
if (options.attempt > ravelInstance.get('redis max retries')) {
ravelInstance.$log.error(`Lost connection to redis: ${code}. Max retry attempts exceeded.`);
// End reconnecting with built in error
return new $err.General(
`Lost connection to redis: ${code}. Max retry attempts reached.`);
} else {
const time = Math.pow(options.attempt, 2) * 100;
ravelInstance.$log.error(`Lost connection to redis: ${code}. Reconnecting in ${time} milliseconds.`);
// reconnect after
return time;
}
};
}
/**
* For disabling redis methods.
*
* @param {object} client - Client to disable a method on.
* @param {string} fn - Name of the function to disable.
* @private
*/
function disable (client, fn) {
client[fn] = function () {
throw new $err.General(
`kvstore cannot use ${fn}(). Use.$kvstore.clone() to retrieve a fresh connection first.`);
};
}
/**
* Returns a fresh connection to Redis.
*
* @param {Ravel} ravelInstance - An instance of a Ravel app.
* @param {boolean} restrict - Iff true, disable `exit`, `subcribe`, `psubscribe`, `unsubscribe` and `punsubscribe`.
* @returns {object} Returns a fresh connection to Redis.
* @private
*/
function createClient (ravelInstance, restrict = true) {
const localRedis = ravelInstance.get('redis port') === undefined || ravelInstance.get('redis host') === undefined;
ravelInstance.on('post init', () => {
ravelInstance.$log.info(localRedis
? 'Using in-memory key-value store. Please do not scale this app horizontally.'
: `Using redis at ${ravelInstance.get('redis host')}:${ravelInstance.get('redis port')}`);
});
let client;
if (localRedis) {
const mock = require('redis-mock');
client = mock.createClient();
client.flushall(); // in case this has been required before
} else {
client = redis.createClient(
ravelInstance.get('redis port'),
ravelInstance.get('redis host'),
{
no_ready_check: true,
retry_strategy: retryStrategy(ravelInstance)
});
}
// log errors
client.on('error', (err) => {
// Use console if framework logging isn't available yet
ravelInstance.$log ? ravelInstance.$log.error(err) : console.error(err);
});
if (ravelInstance.get('redis password')) {
client.auth(ravelInstance.get('redis password'));
}
// keepalive when not testing
const redisKeepaliveInterval = setInterval(() => {
client && client.ping && client.ping();
}, ravelInstance.get('redis keepalive interval'));
ravelInstance.once('end', () => {
clearInterval(redisKeepaliveInterval);
});
if (restrict) {
disable(client, 'quit');
disable(client, 'subscribe');
disable(client, 'psubscribe');
disable(client, 'unsubscribe');
disable(client, 'punsubscribe');
} else {
const origQuit = client.quit;
client.quit = function (...args) {
clearInterval(redisKeepaliveInterval);
return origQuit.apply(client, args);
};
}
client.clone = function () {
return createClient(ravelInstance, false);
};
return client;
}
/**
* Abstraction for redis-like data store.
*
* @param {Ravel} ravelInstance - An instance of a Ravel app.
* @private
*/
module.exports = createClient;
module.exports.retryStrategy = retryStrategy;