Add TTL for KeyValue related features

This commit is contained in:
Simon Ho 2016-08-09 23:21:51 -07:00
parent d7cf478b52
commit 1c20cc83aa
4 changed files with 133 additions and 4 deletions

View File

@ -64,19 +64,23 @@ KeyValueMemoryConnector.prototype._getStoreForModel = function(modelName) {
return this._store[modelName]; return this._store[modelName];
}; };
KeyValueMemoryConnector.prototype.get = KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) {
function(modelName, key, options, callback) {
var store = this._getStoreForModel(modelName); var store = this._getStoreForModel(modelName);
var item = store[key]; var item = store[key];
if (item && item.isExpired()) { if (item && item.isExpired()) {
debug('Removing expired key', key); debug('Removing expired key', key);
delete store[key]; delete store[key];
item = undefined; item = undefined;
} }
};
KeyValueMemoryConnector.prototype.get =
function(modelName, key, options, callback) {
this._removeIfExpired(modelName, key);
var store = this._getStoreForModel(modelName);
var item = store[key];
var value = item ? item.value : null; var value = item ? item.value : null;
debug('GET %j %j -> %s', modelName, key, value); debug('GET %j %j -> %s', modelName, key, value);
if (/^buffer:/.test(value)) { if (/^buffer:/.test(value)) {
@ -127,6 +131,29 @@ function(modelName, key, ttl, options, callback) {
process.nextTick(callback); process.nextTick(callback);
}; };
KeyValueMemoryConnector.prototype.ttl =
function(modelName, key, options, callback) {
this._removeIfExpired(modelName, key);
var store = this._getStoreForModel(modelName);
// key is unknown
if (!(key in store)) {
return process.nextTick(function() {
var err = new Error('Cannot get TTL for unknown key ' + key);
err.statusCode = 404;
callback(err);
});
}
var ttl = store[key].getTtl();
debug('TTL %j %j -> %s', modelName, key, ttl);
process.nextTick(function() {
callback(null, ttl);
});
};
KeyValueMemoryConnector.prototype.disconnect = function(callback) { KeyValueMemoryConnector.prototype.disconnect = function(callback) {
if (this._cleanupTimer) if (this._cleanupTimer)
clearInterval(this._cleanupTimer); clearInterval(this._cleanupTimer);
@ -150,3 +177,7 @@ StoreItem.prototype.setTtl = function(ttl) {
this.expires = undefined; this.expires = undefined;
} }
}; };
StoreItem.prototype.getTtl = function() {
return !this.expires ? undefined : this.expires - Date.now();
};

View File

@ -8,6 +8,7 @@ module.exports = KeyValueAccessObject;
KeyValueAccessObject.get = require('./get'); KeyValueAccessObject.get = require('./get');
KeyValueAccessObject.set = require('./set'); KeyValueAccessObject.set = require('./set');
KeyValueAccessObject.expire = require('./expire'); KeyValueAccessObject.expire = require('./expire');
KeyValueAccessObject.ttl = require('./ttl');
KeyValueAccessObject.getConnector = function() { KeyValueAccessObject.getConnector = function() {
return this.getDataSource().connector; return this.getDataSource().connector;

32
lib/kvao/ttl.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Get remaining expiration (TTL) for a given key.
*
* @param {String} key
* @param {Object} options
* @callback cb
* @param {Error} error
* @param {Number} ttl The remaining TTL for the given key. `undefined` if TTL
* was not initially set.
*
* @header KVAO.ttl(key, cb)
*/
module.exports = function keyValueTtl(key, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
assert(typeof options === 'object', 'options must be an object');
callback = callback || utils.createPromiseCallback();
this.getConnector().ttl(this.modelName, key, options, callback);
return callback.promise;
};

65
test/kvao/ttl.suite.js Normal file
View File

@ -0,0 +1,65 @@
'use strict';
var should = require('should');
var helpers = require('./_helpers');
var Promise = require('bluebird');
module.exports = function(dataSourceFactory, connectorCapabilities) {
describe('ttl', function() {
var CacheItem;
beforeEach(function unpackContext() {
CacheItem = helpers.givenCacheItem(dataSourceFactory);
});
it('returns an error when key does not exist', function() {
return CacheItem.ttl('key-does-not-exist').then(
function() { throw new Error('ttl() should have failed'); },
function(err) {
err.message.should.match(/key-does-not-exist/);
err.should.have.property('statusCode', 404);
});
});
it('returns `undefined` when key does not expire', function() {
return CacheItem.set('a-key', 'a-value')
.then(function() { return CacheItem.ttl('a-key'); })
.then(function(ttl) { should.not.exist(ttl); });
});
context('existing key with expire before expiration time', function() {
it('returns ttl - Callback API', function(done) {
CacheItem.set('a-key', 'a-value', 10, function(err) {
if (err) return done(err);
CacheItem.ttl('a-key', function(err, ttl) {
if (err) return done(err);
ttl.should.be.within(0, 10);
done();
});
});
});
it('returns ttl - Promise API', function() {
return CacheItem.set('a-key', 'a-value', 10)
.delay(1)
.then(function() { return CacheItem.ttl('a-key'); })
.then(function(ttl) { ttl.should.be.within(0, 10); });
});
});
context('existing key with expire after expiration time', function(done) {
it('returns an error', function() {
return CacheItem.set('key-does-not-exist', 'a-value', 10)
.delay(20)
.then(function() {
return CacheItem.ttl('key-does-not-exist');
})
.then(
function() { throw new Error('ttl() should have failed'); },
function(err) {
err.message.should.match(/key-does-not-exist/);
err.should.have.property('statusCode', 404);
});
});
});
});
};