memcache-plus
Version:
Better memcache for node
1,070 lines (917 loc) • 37.4 kB
JavaScript
/**
* @file Main file for a Memcache Connection
*/
var debug = require('debug')('memcache-plus:connection');
var _ = require('lodash'),
carrier = require('carrier'),
misc = require('./misc'),
net = require('net'),
tls = require('tls'),
Immutable = require('immutable'),
Promise = require('bluebird'),
util = require('util'),
EventEmitter = require('events').EventEmitter;
// Note, these first few flags intentionally mirror the memcached module so
// they are cross compatible
var FLAG_JSON = 1<<1;
var FLAG_BINARY = 1<<2;
var FLAG_NUMERIC = 1<<3;
// These are new flags we add for Memcache Plus specific data
var FLAG_COMPRESSED = 1<<4;
var metadataTemp = {};
/**
* Miscellaneous helper functions
*/
/**
* getFlag() - Given a value and whether it's compressed, return the correct
* flag
*
* @param {*} val - the value to inspect and on which to set the flag
* @param {bool} compressed - whether or not this value is to be compressed
* @returns {Number} the value of the flag
*/
function getFlag(val, compressed) {
var flag = 0;
if (typeof val === 'number') {
flag = FLAG_NUMERIC;
} else if (Buffer.isBuffer(val)) {
flag = FLAG_BINARY;
} else if (typeof val !== 'string') {
flag = FLAG_JSON;
}
if (compressed === true) {
flag = flag | FLAG_COMPRESSED;
}
return flag;
}
/**
* formatValue() - Given a value and whether it's compressed, return a Promise
* which will resolve to that value formatted correctly, either compressed or
* not and as the correct type of string
*
* @param {*} val - the value to inspect and on which to set the flag
* @param {bool} compressed - whether or not this value is to be compressed
* @returns {Promise} resolves to the value after being converted and, if
* necessary, compressed
*/
function formatValue(val, compressed) {
var value = val;
if (typeof val === 'number') {
value = val.toString();
} else if (Buffer.isBuffer(val)) {
value = val.toString('binary');
} else if (typeof val !== 'string') {
value = JSON.stringify(val);
}
var bVal = Buffer.from(value);
var compression = Promise.resolve(bVal);
if (compressed) {
// Compress
debug('compression enabled, compressing');
compression = misc.compress(bVal);
}
return compression;
}
/**
* Connection constructor
*
* With the supplied options, connect.
*
* @param {object} opts - The options for this Connection instance
*/
function Connection(opts) {
EventEmitter.call(this);
opts = opts || {};
_.defaults(opts, {
host: 'localhost',
port: '11211',
reconnect: true,
maxValueSize: 1048576,
tls: null
});
this.host = opts.host;
this.port = opts.port;
this.tls = opts.tls;
this.commandBuffer = new Immutable.List();
if (opts.onConnect) {
this.onConnect = opts.onConnect;
}
if (opts.onError) {
this.onError = opts.onError;
} else {
this.onError = function onError(err) { this.emit('error'); console.error(err); };
}
this.bufferBeforeError = opts.bufferBeforeError;
this.netTimeout = ('netTimeout' in opts) ? opts.netTimeout : 500;
this.backoffLimit = ('backoffLimit' in opts) ? opts.backoffLimit : 10000;
this.reconnect = opts.reconnect;
this.disconnecting = false;
this.ready = false;
this.backoff = ('backoff' in opts) ? opts.backoff : 10;
this.maxValueSize = opts.maxValueSize;
this.writeBuffer = new Immutable.List();
this.connect();
}
util.inherits(Connection, EventEmitter);
/**
* Disconnect connection
*/
Connection.prototype.disconnect = function() {
this.ready = false;
this.disconnecting = true;
this.client.end();
};
/**
* Destroy connection immediately
*/
Connection.prototype.destroy = function() {
this.client.destroy();
this.client = null;
this.commandBuffer.forEach(function(deferred) {
deferred.reject(new Error('Memcache connection lost'));
});
this.commandBuffer = this.commandBuffer.clear();
};
/**
* Initialize connection
*
* @api private
*/
Connection.prototype.connect = function() {
var self = this;
var params = {
port: self.port
};
if (self.host) {
params.host = self.host;
}
debug('connecting to host %s:%s', params.host, params.port);
// If a client already exists, we just want to reconnect
if (this.client) {
this.client.connect(params);
} else {
// Initialize a new client, connect
if (self.tls === null) {
self.client = net.connect(params);
} else {
params.tls = self.tls;
self.client = tls.connect(params);
}
self.client.setTimeout(self.netTimeout);
self.client.on('error', self.onError);
self.client.setNoDelay(true);
// If reconnect is enabled, we want to re-initiate connection if it is ended
if (self.reconnect) {
self.client.on('close', function() {
self.emit('close');
self.ready = false;
// Wait before retrying and double each time. Backoff starts at 10ms and will
// plateau at 1 minute.
if (self.backoff < self.backoffLimit) {
self.backoff *= 2;
}
debug('connection to memcache lost, reconnecting in %sms...', self.backoff);
setTimeout(function() {
// Only want to do this if a disconnect was not triggered intentionally
if (!self.disconnecting) {
debug('attempting to reconnect to memcache now.', self.backoff);
self.destroy();
self.connect();
}
}, self.backoff);
});
}
}
self.client.on('connect', function() {
self.emit('connect');
debug('successfully (re)connected!');
self.ready = true;
// Reset backoff if we connect successfully
self.backoff = 10;
// If an onConnect handler was specified, execute it
if (self.onConnect) {
self.onConnect();
}
self.flushBuffer();
});
carrier.carry(self.client, self.read.bind(self));
};
/**
* read() - Called as soon as we get data back from this connection from the
* server. The response parsing is a bit of a beast.
*/
Connection.prototype.read = function(data) {
debug('got data: "%s" and the queue now has "%d" elements',
misc.truncateIfNecessary(data),
this.commandBuffer.size
);
var deferred = this.commandBuffer.first();
var done = true;
var err = null;
var resp = data;
if (deferred.type === 'autodiscovery') {
this.autodiscover = this.autodiscover || {};
done = false;
// Treat this a bit like a special snowflake
if (data.match(/^CONFIG .+/)) {
// Do nothing with this for now
var configItems = data.match(/^CONFIG cluster ([0-9]+) ([0-9]+)/);
this.autodiscover.len = Number(configItems[2]);
} else if (this.autodiscover.len !== undefined && this.autodiscover.version === undefined && data.match(/^([0-9])+/)) {
this.autodiscover.version = Number(data);
} else if (data.match(/^END$/)) {
resp = this.autodiscover;
this.autodiscover = null;
done = true;
} else if (data.length > 20) {
this.autodiscover.info = data;
}
} else if (data.match(/^ERROR$/) && this.commandBuffer.size > 0) {
debug('got an error from memcached');
// We only want to do this if the last thing was not an error,
// as if it were, we already would have notified about the error
// last time so now we want to ignore it
err = new Error(util.format('Memcache returned an error: %s\r\nFor key %s', data, deferred.key));
} else if (data.match(/^VALUE .+/)) {
var spl = data.match(/^VALUE ([^ ]+) ([0-9]+) ([0-9]+)( ([0-9]+))?$/);
debug('Got some metadata', spl);
metadataTemp[spl[1]] = {
flag: Number(spl[2]),
len: Number(spl[3])
};
if (spl.length === 6) {
metadataTemp[spl[1]].cas = spl[5];
}
done = false;
} else if (data.match(/^END$/)) {
if (deferred.type === 'items') {
resp = this.data;
} else if (metadataTemp[deferred.key]) {
var metadata = metadataTemp[deferred.key];
resp = [ this.data, metadata.flag, metadata.len, metadata.cas ];
// After we've used this metadata, purge it
delete metadataTemp[deferred.key];
} else {
resp = [ this.data ];
}
} else if (data.match(/^STAT items.+/)) {
// format is
// STAT items:SLAB_ID:<key> <value>
var splData = data.match(/^STAT items:([0-9]+):(.+) ([0-9]+)$/);
var slabId = splData[1];
var slabName = splData[2];
var slabValue = splData[3];
if (this.data) {
resp = this.data;
} else {
resp = {
items: {},
ids: []
};
}
if (!resp.items[slabId]) {
resp.items[slabId] = {};
resp.ids.push(slabId);
}
// set the slab key and value to the slab id
resp.items[slabId][slabName] = parseInt(slabValue, 10);
done = false;
} else if (data.match(/^ITEM .+/)) {
// ITEM <item_key> [<item_size> b; <expiration_timestamp> s]
var cacheData = data.match(/^ITEM\s(.+)\s\[(\d+)\sb\;\s(\d+)\ss\]$/);
var key = cacheData[1];
// Item size (including key) in bytes
var itemSize = parseInt(cacheData[2], 10);
// Expiration timestamp in second
var expiry = parseInt(cacheData[3], 10);
if (this.data) {
resp = this.data;
} else {
resp = {
items: {},
ids: []
};
}
resp.items[key] = {
key: key,
bytes: itemSize,
expiry_time_secs: expiry
};
resp.ids.push(key);
done = false;
} else if (data.match(/^SERVER_ERROR|CLIENT_ERROR .+/)) {
err = new Error(util.format('Memcache returned an error: %s', data));
} else if (data.match(/^VERSION .+/)) {
resp = data.match(/^VERSION ([0-9|\.]+)/)[1];
} else {
// If this is a special response that we expect, handle it
if (data.match(/^(STORED|NOT_STORED|DELETED|EXISTS|TOUCHED|NOT_FOUND|OK|INCRDECR|STAT)$/)) {
// Do nothing currently...
debug('misc response, passing along to client');
} else {
if (data !== '') {
if (deferred.type !== 'incr' && deferred.type !== 'decr') {
done = false;
}
}
}
}
if (done) {
// Pull this guy off the queue
this.commandBuffer = this.commandBuffer.shift();
// Reset for next loop
this.data = null;
} else {
this.data = resp;
}
if (err) {
// If we have an error, reject
deferred.reject(err);
} else {
// If we don't have an error, resolve if done
if (done) {
deferred.resolve(resp);
}
}
debug('responded and the queue now has "%s" elements', this.commandBuffer.size);
};
/**
* flushBuffer() - Flush the queue for this connection
*/
Connection.prototype.flushBuffer = function() {
debug('trying to flush buffer for %s', this.host);
if (this.writeBuffer && this.writeBuffer.size > 0) {
debug('flushing write buffer for %s', this.host);
// @todo Watch out for and handle how this behaves with a very long buffer
while(this.writeBuffer.size > 0) {
this.client.write(this.writeBuffer.first());
this.writeBuffer = this.writeBuffer.shift();
this.client.write('\r\n');
}
debug('flushed write buffer for %s', this.host);
}
};
/**
* write() - Write a command to this connection
*/
Connection.prototype.write = function(str) {
// If for some reason this connection is not yet ready and a request is tried,
// we don't want to fire it off so we write it to a buffer and then will fire
// them off when we finally do connect. And even if we are connected we don't
// want to fire off requests unless the write buffer is emptied. So if say we
// buffer 100 requests, then connect and chug through 10, there are 90 left to
// be flushed before we send it new requests so we'll just keep pushing on the
// end until it's flushed
if (this.ready && this.writeBuffer.size < 1) {
debug('sending: "%s"', misc.truncateIfNecessary(str));
this.client.write(str);
this.client.write('\r\n');
} else if (this.writeBuffer.size < this.bufferBeforeError) {
debug('buffering to [%s]: "%s" ', this.host, misc.truncateIfNecessary(str));
this.writeBuffer = this.writeBuffer.push(str);
// Check if we should flush this queue. Useful in case it gets stuck for
// some reason
if (this.ready) {
this.flushBuffer();
}
} else {
this.commandBuffer.first().reject('Error, Connection to memcache lost and buffer over ' + this.bufferBeforeError + ' items');
this.commandBuffer = this.commandBuffer.shift();
}
};
Connection.prototype.autodiscovery = function() {
debug('starting autodiscovery');
var deferred = misc.defer('autodiscovery');
deferred.type = 'autodiscovery';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write('config get cluster');
return deferred.promise
.then(function(config) {
debug('got autodiscovery response from elasticache', config);
// Elasticache returns hosts as a string like the following:
// victor.di6cba.0001.use1.cache.amazonaws.com|10.10.8.18|11211 victor.di6cba.0002.use1.cache.amazonaws.com|10.10.8.133|11211
// We want to break it into the correct pieces
var hosts = config.info.toString().split(' ');
return hosts.map(function(host) {
host = host.split('|');
return util.format('%s:%s', host[0], host[2]);
});
});
};
/**
* set() - Set a value on this connection
*/
Connection.prototype.set = function(key, val, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
// Note, we use base64 encoding because this allows us to compress it
// but also safely store/retrieve it in memcache. Not quite as efficient
// as if we just used the zlib default, but it also includes a bunch of
// funky characters that didn't seem to be safe to set/get from memcache
// without messing with the data.
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize));
}
// What we're doing here is a bit tricky as we need to invert control.
// We are going to basically return a Promise that itself is made up of a
// chain of Promises, most resolved here (the initial communication with
// memcache), but the last is not resolved until some time in the future.
// This Promise is put into a queue which will be processed whenever the
// socket responds (usually immediately). This because we don't know
// exactly when it's going to respond since it's an event emitter. So we
// are doing some funky promise trickery to convert event emmitter into
// Promise/or Callback. Since all actions in this library share the same
// queue, order should be maintained and this trick should work!
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('set %s %d %d %d', key, flag, ttl, v.length));
// Then the actual value (as a string)
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the set. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve();
}
});
});
};
/**
* cas() - Set a value on this connection if the CAS value matches
*/
Connection.prototype.cas = function(key, val, cas, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
// Note, we use base64 encoding because this allows us to compress it
// but also safely store/retrieve it in memcache. Not quite as efficient
// as if we just used the zlib default, but it also includes a bunch of
// funky characters that didn't seem to be safe to set/get from memcache
// without messing with the data.
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize));
}
// What we're doing here is a bit tricky as we need to invert control.
// We are going to basically return a Promise that itself is made up of a
// chain of Promises, most resolved here (the initial communication with
// memcache), but the last is not resolved until some time in the future.
// This Promise is put into a queue which will be processed whenever the
// socket responds (usually immediately). This because we don't know
// exactly when it's going to respond since it's an event emitter. So we
// are doing some funky promise trickery to convert event emmitter into
// Promise/or Callback. Since all actions in this library share the same
// queue, order should be maintained and this trick should work!
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('cas %s %d %d %d %s', key, flag, ttl, v.length, cas));
// Then the actual value (as a string)
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if ((data === 'EXISTS') || (data === 'NOT_FOUND')) {
return Promise.resolve(false);
} else if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the set. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve(true);
}
});
});
};
function convert(data, flag, length) {
if (flag & FLAG_NUMERIC) {
return Number(data);
} else if (flag & FLAG_BINARY) {
var buff = Buffer.alloc(length);
buff.write(data, 0, 'binary');
return buff;
} else if (flag & FLAG_JSON) {
return JSON.parse(data);
} else {
return data.toString();
}
}
/**
* incr() - Increment a value on this connection
*/
Connection.prototype.incr = function(key, amount) {
// Do the delete
var deferred = misc.defer(key);
deferred.type = 'incr';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('incr %s %d', key, amount));
return deferred.promise
.then(function(v) {
if (v === 'NOT FOUND') {
throw new Error('key %s not found to incr', key);
} else {
return Number(v);
}
})
.timeout(this.netTimeout);
};
/**
* decr() - Decrement a value on this connection
*/
Connection.prototype.decr = function(key, amount) {
// Do the delete
var deferred = misc.defer(key);
deferred.type = 'decr';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('decr %s %d', key, amount));
return deferred.promise
.then(function(v) {
if (v === 'NOT FOUND') {
throw new Error('key %s not found to decr', key);
} else {
return Number(v);
}
})
.timeout(this.netTimeout);
};
/**
* gets() - Get a value and CID on this connection
*
* @param {String} key - The key for the value to retrieve
* @param {Object} [opts] - Any additional options for this get
* @returns {Promise} - containing an array with the value and CID
*/
Connection.prototype.gets = function(key, opts) {
opts = opts || {};
// Do the gets
var deferred = misc.defer(key);
this.commandBuffer = this.commandBuffer.push(deferred);
this.write('gets ' + key);
return deferred.promise
.timeout(this.netTimeout)
.spread(function(data, flag, length, cas) {
if (data) {
if (opts.compressed || (flag & FLAG_COMPRESSED)) {
// @todo compressed data should still be able to utilize the
// flags to return data of same type set
return misc.decompress(Buffer.from(data, 'base64'))
.then(function(d) {
return [ d.toString(), cas ];
})
.catch(function(err) {
if (err.toString().indexOf('Error: incorrect header check') > -1) {
// Basically we get this OperationalError when we try to decompress a value
// which was not previously compressed. We return null instead of bubbling
// this error up the chain because we want to represent it as a cache miss
// rather than an actual error. By looking like a cache miss like this, it
// makes it so someone can turn on compression and it'll work fine, looking
// like a cache miss. So in the usual workflow the client tries to set it
// again which means the second time around the compressed version is set
// as the client wanted/expected.
return [ null, null ];
} else {
throw err;
}
});
} else {
return [ convert(data, flag, length), cas ];
}
} else {
return [ null, null ];
}
});
};
/**
* get() - Get a value on this connection
*
* @param {String} key - The key for the value to retrieve
* @param {Object} [opts] - Any additional options for this get
* @returns {Promise}
*/
Connection.prototype.get = function(key, opts) {
opts = opts || {};
// Do the get
var deferred = misc.defer(key);
this.commandBuffer = this.commandBuffer.push(deferred);
this.write('get ' + key);
return deferred.promise
.timeout(this.netTimeout)
.spread(function(data, flag, length) {
if (data) {
if (opts.compressed || (flag & FLAG_COMPRESSED)) {
// @todo compressed data should still be able to utilize the
// flags to return data of same type set
return misc.decompress(Buffer.from(data, 'base64'))
.then(function(d) {
return d.toString();
})
.catch(function(err) {
if (err.toString().indexOf('Error: incorrect header check') > -1) {
// Basically we get this OperationalError when we try to decompress a value
// which was not previously compressed. We return null instead of bubbling
// this error up the chain because we want to represent it as a cache miss
// rather than an actual error. By looking like a cache miss like this, it
// makes it so someone can turn on compression and it'll work fine, looking
// like a cache miss. So in the usual workflow the client tries to set it
// again which means the second time around the compressed version is set
// as the client wanted/expected.
return null;
} else {
throw err;
}
});
} else {
return convert(data, flag, length);
}
} else {
return null;
}
});
};
/**
* flush() - delete all values on this connection
* @param delay
* @returns {Promise}
*/
Connection.prototype.flush_all = function(delay) {
var deferred = misc.defer(delay);
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('flush_all %s', delay));
return deferred.promise
.then(function(v) {
return v === 'OK';
});
};
/**
* add() - Add a value on this connection
*/
Connection.prototype.add = function(key, val, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize));
}
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('add %s %d %d %d', key, flag, ttl, v.length));
// Then the actual value
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if (data === 'NOT_STORED') {
throw new Error(util.format('Cannot "add" for key "%s" because it already exists', key));
} else if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the add. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve();
}
});
});
};
/**
* replace() - Replace a value on this connection
*/
Connection.prototype.replace = function(key, val, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to replace in memcache: %s > %s', v.length, self.maxValueSize));
}
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('replace %s %d %d %d', key, flag, ttl, v.length));
// Then the actual value
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if (data === 'NOT_STORED') {
throw new Error(util.format('Cannot "replace" for key "%s" because it does not exist', key));
} else if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the replace. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve();
}
});
});
};
/**
* append() - Append a value on this connection
*/
Connection.prototype.append = function(key, val, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to append in memcache: %s > %s', v.length, self.maxValueSize));
}
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('append %s %d %d %d', key, flag, ttl, v.length));
// Then the actual value
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if (data === 'NOT_STORED') {
throw new Error(util.format('Cannot "append" for key "%s" because it does not exist', key));
} else if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the append. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve();
}
});
});
};
/**
* prepend() - Prepend a value on this connection
*/
Connection.prototype.prepend = function(key, val, ttl) {
var self = this;
ttl = ttl || 0;
var opts = {};
if (_.isObject(ttl)) {
opts = ttl;
ttl = opts.ttl || 0;
}
var flag = getFlag(val, opts.compressed);
return formatValue(val, opts.compressed)
.bind(this)
.then(function(v) {
if (opts.compressed) {
v = Buffer.from(v.toString('base64'));
}
if (v.length > self.maxValueSize) {
throw new Error(util.format('Value too large to prepend in memcache: %s > %s', v.length, self.maxValueSize));
}
var deferred = misc.defer(key);
deferred.key = key;
this.commandBuffer = this.commandBuffer.push(deferred);
// First send the metadata for this request
this.write(util.format('prepend %s %d %d %d', key, flag, ttl, v.length));
// Then the actual value
this.write(util.format('%s', v));
return deferred.promise
.then(function(data) {
// data will be a buffer
if (data === 'NOT_STORED') {
throw new Error(util.format('Cannot "prepend" for key "%s" because it does not exist', key));
} else if (data !== 'STORED') {
throw new Error(util.format('Something went wrong with the prepend. Expected STORED, got :%s:', data.toString()));
} else {
return Promise.resolve();
}
});
});
};
/**
* delete() - Delete value for this key on this connection
*
* @param {String} key - The key to delete
* @returns {Promise}
*/
Connection.prototype.delete = function(key) {
// Do the delete
var deferred = misc.defer(key);
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('delete %s', key));
return deferred.promise
.then(function(v) {
if (v === 'DELETED') {
return true;
} else {
return false;
}
})
.timeout(this.netTimeout);
};
/**
* 'stats items'() - return items statistics
* @returns {Promise}
*/
Connection.prototype['stats items'] = function() {
debug('stats items');
var deferred = misc.defer();
deferred.type = 'items';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('stats items'));
return deferred.promise
.then(function(slabData) {
var slabItems = [];
// slabData.items - object containing slab data
// slabData.ids - array of slab ids in which the results came in
if (slabData && slabData.ids) {
slabData.ids.forEach(function(slabId) {
var item = {};
item.slab_id = parseInt(slabId, 10);
item.data = slabData.items[slabId];
item.server = this.host + ':' + this.port;
slabItems.push(item);
}.bind(this));
}
return slabItems;
}.bind(this));
};
/**
* 'stats cachedump'() - dump cache info for a given slabs id
* @param {number} slabsId
* @param {number} limit number of entries to show
*/
Connection.prototype['stats cachedump'] = function(slabsId, limit) {
var deferred = misc.defer(slabsId);
deferred.type = 'items';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write(util.format('stats cachedump %s %s', slabsId, limit));
return deferred.promise
.then(function(cacheMetaData) {
var cacheItems = [];
// cacheMetaData.items - object containing cache metadata
// cacheMetaData.ids - array of cache keys in which the results came in
if (cacheMetaData && cacheMetaData.ids) {
cacheMetaData.ids.forEach(function(cacheKey) {
cacheItems.push(cacheMetaData.items[cacheKey]);
}.bind(this));
}
return cacheItems;
}.bind(this));
};
/**
* version() - Get current Memcached version from the server
*
* @returns {Promise}
*/
Connection.prototype.version = function() {
var deferred = misc.defer();
deferred.type = 'version';
this.commandBuffer = this.commandBuffer.push(deferred);
this.write('version');
return deferred.promise;
};
module.exports = Connection;