UNPKG

cache-service-memcached

Version:
285 lines (267 loc) 9.93 kB
var namespace = 'cache-module:memcached'; var debug = require('debug-levels')(namespace); var Memcached = require('memcached'); var noop = function () { }; /** * MemcachedCacheModule constructor * @constructor * * @param locations {string|array|object} - Memcached servers location(s). @see https://www.npmjs.com/package/memcached#server-locations * @param options {object: { * verbose {boolean} Default: false - enables debug logging * defaultLifetime {number} Default: 90 - default lifetime value in seconds * backgroundRefreshIntervalCheck {boolean} Default: `true` - enables `backgroundRefreshInterval` validation. **Background refresh only** * backgroundRefreshInterval {number} Default: 60000 - frequency of checking items for background refresh. This value should be less than `backgroundRefreshMinTtl`. **Background refresh only** * backgroundRefreshMinTtl {number} Default: 70000 - determines a check time for background refresh. Items that expire in less than this value, will be re-fetched during background refresh cycle. **Background refresh only** * memcachedOptions: { * @see rest of the options in memcached npm module reference https://www.npmjs.com/package/memcached#options * } * }} - cache module memcached configuration */ function MemcachedCacheModule (locations, options) { options = options || {}; if (!(this instanceof MemcachedCacheModule)) { return new MemcachedCacheModule(locations, options); } this.type = 'memcached-cache-module'; this.storage = 'memcached'; if (options.verbose) { require('debug').enable(namespace); } this._backgroundRefreshEnabled = false; this._defaultLifetime = options.defaultLifetime || 90; this._refreshKeys = {}; this._backgroundRefreshIntervalCheck = (typeof options.backgroundRefreshIntervalCheck === 'boolean') ? options.backgroundRefreshIntervalCheck : true; this._backgroundRefreshInterval = options.backgroundRefreshInterval || 60000; this._backgroundRefreshMinTtl = options.backgroundRefreshMinTtl || 70000; var memcachedOptions = options.memcachedOptions || {}; debug.info('Connecting to memcached: \n%O', locations); this.memcachedClient = new Memcached(locations, memcachedOptions); this.memcachedClient.on('failure', function (details) { var msg = 'Memcached server ' + details.server + ' went down due to: \n' + details.messages.join(); throw new Error(msg); }); this.memcachedClient.on('reconnecting', function (details) { debug.warn( 'Total downtime caused by server ' + details.server + ': ' + details.totalDownTime + 'ms' ); }); this.memcachedClient.on('issue', function (details) { debug.warn( 'Total downtime caused by server ' + details.server + ': ' + details.totalDownTime + 'ms' ); }); this.memcachedClient.on('reconnect', function (details) { debug.warn( 'Total downtime caused by server ' + details.server + ': ' + details.totalDownTime + 'ms' ); }); this.memcachedClient.on('remove', function (details) { debug.warn( 'Total downtime caused by server ' + details.server + ': ' + details.totalDownTime + 'ms' ); }); } MemcachedCacheModule.prototype.add = function (key, value, lifetime, cb) { cb = cb || noop; lifetime = lifetime || this._defaultLifetime; debug.verbose('add() called with \n key=%s\n lifetime=%d seconds', key, lifetime); this.memcachedClient.add(key, value, lifetime, function (err, result) { if (err) { debug.error('add(): Unable to store ' + key + ' to memcached, err:', err); } else { debug.verbose('add():success key=%s', key); } cb(err, result); }); }; MemcachedCacheModule.prototype.get = function (key, cb, cleanKey) { var cacheKey = cleanKey || key; debug.verbose('get() called with key=%s', cacheKey); this.memcachedClient.get(cacheKey, function (err, result) { if (err) { debug.error('Unable to get key=' + cacheKey + ' from memcached, err:', err); } if (result) { debug.verbose('get():hit key=%s', cacheKey); return cb(err, result); } debug.verbose('get():miss key=%s', cacheKey); cb(err, null); }); }; MemcachedCacheModule.prototype.mget = function (keys, cb, index) { debug.verbose('mget() called with keys=%O', keys); this.memcachedClient.getMulti(keys, function (err, response) { for (var keyIndex = 0; keyIndex < keys.length; keyIndex++) { var key = keys[keyIndex]; if (response.hasOwnProperty(key)) { try { response[key] = JSON.parse(response[key]); } catch (err) { debug.error('mget(): Error parsing JSON for key=%s, \nerr: %s', key, err); } } else { response[key] = null; } } cb(err, response, index); }); }; MemcachedCacheModule.prototype.set = function () { var _this = this; var key = arguments[0]; var value = arguments[1]; var lifetime = arguments[2] || this._defaultLifetime; var refreshCb = (arguments.length === 5) ? arguments[3] : null; var cb = arguments[arguments.length - 1]; if (typeof cb !== 'function') { cb = noop; } debug.verbose('set() called with key=%s, lifetime=%ss', key, lifetime); this.memcachedClient.set(key, value, lifetime, function (err, result) { if (err) { debug.error('set(): Unable to store %s to memcached, \nerr: %s', key, err); } else { debug.verbose('set():success key=%s', key); if (refreshCb) { var expirationDate = (lifetime * 1000) + Date.now(); _this._refreshKeys[key] = { expirationDate: expirationDate, lifeSpan: lifetime, refreshCb: refreshCb }; _this._backgroundRefreshInit(); } } cb(err, result); }); }; MemcachedCacheModule.prototype.mset = function (obj, lifetime, cb) { var _this = this; var promises = []; cb = cb || noop; for (var key in obj) { if (obj.hasOwnProperty(key)) { var tempExpiration = lifetime || this._defaultLifetime; var value = obj[key]; if (typeof value === 'object' && value.cacheValue) { tempExpiration = value.lifetime || tempExpiration; value = value.cacheValue; } try { value = JSON.stringify(value); } catch (err) { debug.error('mset(): Error converting value to string for key=%s, err: %s', key, err); } promises.push((function (key, value, tempExpiration) { return new Promise(function (resolve, reject) { return _this.set(key, value, tempExpiration, function (err, result) { if (err) { return reject(err); } return resolve(err, result); }); }); })(key, value, tempExpiration)); // reference local vars instead of mutable ones. } } Promise.all(promises) .then(function (results) { cb(null, results); }) .catch(function (error) { cb(error); }); }; MemcachedCacheModule.prototype.del = function (key, cb) { var _this = this; cb = cb || noop; debug.verbose('del() called with key=%s', key); if (typeof key === 'string') { this.memcachedClient.del(key, cb); } else if (Array.isArray(key)) { Promise.all(key.map(function (key) { return new Promise(function (resolve, reject) { return _this.del(key, function (err, result) { if (err) { return reject(err); } return resolve(err, result); }); }); })) .then(function (results) { cb(null, results); }) .catch(function (error) { cb(error); }); } else { cb(new Error('`key` type should be either String or Array')); } }; MemcachedCacheModule.prototype.flush = function (cb) { debug.verbose('flush() called'); if (this._refreshInterval) { this._backgroundRefreshEnabled = false; clearTimeout(this._refreshInterval); } this.memcachedClient.flush(cb || noop); }; /** * Handle the refresh callback from the consumer, save the data to memcached. * * @param {string} key The key used to save. * @param {Object} data refresh keys data. * @param {Error|null} err consumer callback failure. * @param {*} response The consumer response. */ MemcachedCacheModule.prototype._handleRefreshResponse = function (key, data, err, response) { if (!err) { this.set(key, response, data.lifeSpan, data.refresh, noop); } }; /** * Refreshes all keys that were set with a refresh function */ MemcachedCacheModule.prototype._backgroundRefresh = function () { var keys = Object.keys(this._refreshKeys); var now = Date.now(); keys.forEach(function (key) { var cachedItemMetadata = this._refreshKeys[key]; if (cachedItemMetadata.expirationDate - now < this._backgroundRefreshMinTtl) { cachedItemMetadata.refreshCb( key, this._handleRefreshResponse.bind(this, key, cachedItemMetadata) ); } }, this); }; /** * Initialize background refresh */ MemcachedCacheModule.prototype._backgroundRefreshInit = function () { if (!this._backgroundRefreshEnabled) { debug('Background refresh is enabled with following parameters: \n' + ' backgroundRefreshIntervalCheck=%d \n' + ' backgroundRefreshMinTtl=%d \n' + ' backgroundRefreshInterval=%d', this._backgroundRefreshIntervalCheck, this._backgroundRefreshMinTtl, this._backgroundRefreshInterval ); this._backgroundRefreshEnabled = true; if (this._backgroundRefreshIntervalCheck) { if (this._backgroundRefreshInterval > this._backgroundRefreshMinTtl) { throw new Error('BACKGROUND_REFRESH_INTERVAL_EXCEPTION', 'backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.'); } } this._refreshInterval = setInterval( this._backgroundRefresh.bind(this), this._backgroundRefreshInterval ); } }; module.exports = MemcachedCacheModule;