Skip to content

Commit

Permalink
Implement cas command
Browse files Browse the repository at this point in the history
  • Loading branch information
ysugimoto committed Mar 25, 2017
1 parent a47462f commit 45cd84c
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 18 deletions.
63 changes: 53 additions & 10 deletions libs/memcached.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const DEFAULT_OPTIONS = {
host: 'localhost',
port: 11211,
autoreconnect: false,
commandTimeout: 1000,
reconnectDuration: 2000,
maxRetryConnectCount: 10
};
Expand Down Expand Up @@ -202,16 +203,48 @@ class Memcached extends EventEmitter {
}

/**
* Get "single" cache data from supplied key
* Send "cas" command
*
* @param {String} key cache key
* @param {String|Buffer} value store value
* @param {Booelan} isCompress flag of data should be compressed
* @param {Number} expires time to cache has expired (if supplied zero, the value is persitent)
* @param {String} unique cas unique
* @return {Promise} -
* @resolve {String} reply code
* @reject {String} reply code
*/
cas(key, value, isCompress = 0, expires = 0, unique = '') {
this.validateKey(key);
const byteSize = (value instanceof Buffer) ? Buffer.length : Buffer.byteLength(value, 'utf8');
const command = [
`cas ${key} ${isCompress ? 1 : 0} ${expires} ${byteSize} ${unique}`,
(value instanceof Buffer) ? value.toString('utf8') : value
];
return this.conn.command(command)
.then(message => {
const code = message.code;
switch (code) {
case Message.STORED:
return Promise.resolve(code);
default:
return Promise.reject(code);
}
})
;
}

/**
* Get cache data from supplied key
*
* @param {Array} keys cache key
* @return {Promise} -
* @resolve {String|null} cache data
* @reject {Void}
*/
get(key) {
this.validateKey(key);
return this.conn.command([`get ${key}`])
get(...keys) {
keys.forEach(k => this.validateKey(k));
return this.conn.command([`get ${keys.join(' ')}`])
.then(message => {
const code = message.code;
switch (code) {
Expand All @@ -222,14 +255,23 @@ class Memcached extends EventEmitter {
case Message.CLIENT_ERROR:
return Promise.reject(code);
default:
return Promise.resolve(message.getValue());
if (keys.length === 1) {
return Promise.resolve(message.getValue());
}
const values = message.getBulkValues();
keys.forEach(k => {
if (!values.hasOwnProperty(k)) {
values[k] = null;
}
});
return Promise.resolve(values);
}
})
;
}

/**
* Get "multiple" cache data from supplied key
* Get cache data from supplied key with cas unique
*
* @param {String} key cache key
* @return {Promise} -
Expand All @@ -238,8 +280,7 @@ class Memcached extends EventEmitter {
*/
gets(...keys) {
keys.forEach(k => this.validateKey(k));
keys.unshift('get');
return this.conn.command([keys.join(' ')])
return this.conn.command([`gets ${keys.join(' ')}`])
.then(message => {
const code = message.code;
switch (code) {
Expand All @@ -250,8 +291,10 @@ class Memcached extends EventEmitter {
case Message.CLIENT_ERROR:
return Promise.reject(code);
default:
const values = message.getBulkValues();
keys.shift();
if (keys.length === 1) {
return Promise.resolve(message.getObjectValue());
}
const values = message.getBulkObjectValues();
keys.forEach(k => {
if (!values.hasOwnProperty(k)) {
values[k] = null;
Expand Down
47 changes: 41 additions & 6 deletions libs/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,51 @@ class Message {
* @return {String} -
*/
getValue() {
return this.getObjectValue().value;
}

/**
* Get parsed multiple-value from "get" command response
*
* @return {Object} -
*/
getBulkValues() {
const values = this.getBulkObjectValues();
const ret = {};
Object.keys(values).forEach(k => {
ret[k] = values[k].value;
});

return ret;
}

/**
* Get parsed value from "gets" command response contains "cas unique"
*
* @return {Object} -
*/
getObjectValue() {
const buffer = this.buffer;
let start = buffer.indexOf(CRLF);
const meta = buffer.slice(0, start).toString('utf8').split(' ');
start += CRLF_LENGTH;
const value = buffer.slice(start, start + parseInt(meta[3], 10));

return value.toString('utf8');
return {
key: meta[1],
flags: meta[2],
bytes: meta[3],
value: value.toString('utf8'),
cas: meta[4] || null
};
}

/**
* Get parsed multiple-value from "gets" command response
* Get parsed multiple-value from "gets" command response contains "cas unique"
*
* @return {String} -
* @return {Object} -
*/
getBulkValues() {
getBulkObjectValues() {
const values = {};
const buffer = this.buffer;
let index = 0;
Expand All @@ -152,13 +182,18 @@ class Message {
}
index = delim + CRLF_LENGTH;
const dataSize = parseInt(meta[3], 10);
values[meta[1]] = buffer.slice(index, index + dataSize).toString('utf8');
values[meta[1]] = {
key: meta[1],
flags: meta[2],
bytes: meta[3],
value: buffer.slice(index, index + dataSize).toString('utf8'),
cas: meta[4] || null
};
index += dataSize + CRLF_LENGTH;
} while(true);

return values;
}

}

Message.STORED = 'STORED';
Expand Down
30 changes: 28 additions & 2 deletions tests/memcached.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,32 @@ describe('Memcached Class', () => {
});
});

describe('#cas', () => {
const rand = Math.floor(Math.random() * (1e10 - 1) + 1);
const a = `a_${rand}`;
const dat = 'cas data';

beforeEach(() => {
return client.set(a, dat);
});

it('should update value if cas unique matched', () => {
return client.gets(a)
.then(code => {
const cas = code.cas;
return client.cas(a, 'cas data updated', 0, 100, cas);
})
.then(code => expect(code).to.equal('STORED'));
});
it('should not update value if cas unique does not matched', () => {
return client.gets(a)
.then(code => {
return client.cas(a, 'cas data updated', 0, 100, '9999');
})
.catch(code => expect(code).to.equal('EXISTS'));
});
});

describe('#get', () => {
const rand = Math.floor(Math.random() * (1e10 - 1) + 1);
const a = `a_${rand}`;
Expand Down Expand Up @@ -196,8 +222,8 @@ describe('Memcached Class', () => {
return client.gets(a, b)
.then(value => {
expect(value).to.have.keys([a, b]);
expect(value[a]).to.equal(dat01);
expect(value[b]).to.equal(dat02);
expect(value[a].value).to.equal(dat01);
expect(value[b].value).to.equal(dat02);
});
});
});
Expand Down
31 changes: 31 additions & 0 deletions tests/message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,35 @@ describe('Message Class', () => {
expect(values.lorem).to.equal('ipsum');
});
});

describe('#getObjectValue', () => {
beforeEach(() => {
m.append(Buffer.from('VALUE foo 0 3 100\r\nbar\r\nEND\r\n', 'utf8'));
m.freeze();
});

it('should return Object that has expected keys', () => {
const value = m.getObjectValue();
expect(value).have.keys(['key', 'flags', 'value', 'bytes', 'cas']);
expect(value.cas).to.equal('100');
expect(value.value).to.equal('bar');
});
});

describe('#getBulkObjectValues', () => {
beforeEach(() => {
m.append(Buffer.from('VALUE foo 0 3 100\r\nbar\r\n', 'utf8'));
m.append(Buffer.from('VALUE lorem 0 5 200\r\nipsum\r\nEND\r\n', 'utf8'));
m.freeze();
});

it('should return parsed bulk value as object', () => {
const values = m.getBulkObjectValues();
expect(values).have.keys(['foo', 'lorem']);
expect(values.foo).have.keys(['key', 'flags', 'value', 'bytes', 'cas']);
expect(values.foo.value).to.equal('bar');
expect(values.lorem).have.keys(['key', 'flags', 'value', 'bytes', 'cas']);
expect(values.lorem.value).to.equal('ipsum');
});
});
});

0 comments on commit 45cd84c

Please sign in to comment.