diff --git a/lib/CacheCluster.js b/lib/CacheCluster.js index 3e62c9d..8be5a73 100644 --- a/lib/CacheCluster.js +++ b/lib/CacheCluster.js @@ -149,6 +149,12 @@ CacheCluster.prototype.get = function (key) { return this._wrapPromiseWithProfiling(cacheInstance.get(key), 'get') } +/** @override */ +CacheCluster.prototype.incr = function (key, increment) { + var cacheInstance = this._servers[this._hashRing.get(key)] + return this._wrapPromiseWithProfiling(cacheInstance.incr(key, increment), 'set') +} + /** @override */ CacheCluster.prototype.set = function (key, val, maxAgeMs) { var cacheInstance = this._servers[this._hashRing.get(key)] diff --git a/lib/CacheInstance.js b/lib/CacheInstance.js index 2aa9801..420e840 100644 --- a/lib/CacheInstance.js +++ b/lib/CacheInstance.js @@ -96,6 +96,17 @@ CacheInstance.prototype.get = function (key) { throw new Error("get() must be implemented by any class extending CacheInstance") } +/** + * Increment a key. + * + * @param {string} key The key to set + * @param {number=} increment The value to increment by. 1 if unspecified. + * @return {Q.Promise} + */ +CacheInstance.prototype.incr = function (key, increment) { + throw new Error("incr() must be implemented by any class extending CacheInstance") +} + /** * Set a key. * diff --git a/lib/CachePair.js b/lib/CachePair.js index 3177fad..de4ff15 100644 --- a/lib/CachePair.js +++ b/lib/CachePair.js @@ -138,6 +138,13 @@ CachePair.prototype.get = function (key) { }) } +/** @override */ +CachePair.prototype.incr = function (key, increment) { + var promises = [this._primary.incr(key, increment)] + if (this._maybeOnSecondary(key)) promises.push(this._secondary.incr(key, increment)) + return Q.all(promises) +} + /** @override */ CachePair.prototype.set = function (key, val, maxAgeMs, setWhenNotExist) { var promises = [this._primary.set(key, val, maxAgeMs, setWhenNotExist)] diff --git a/lib/FakeCache.js b/lib/FakeCache.js index ff1da9d..b8ce1f4 100644 --- a/lib/FakeCache.js +++ b/lib/FakeCache.js @@ -75,6 +75,28 @@ FakeCache.prototype.del = function (key) { }) } +/** @override */ +FakeCache.prototype.incr = function (key, increment) { + if (this._failureCount > 0) return this._fakeFail('set') + this._logger.fine('FakeCache - incr', key, increment) + + if (increment === undefined) { + increment = 1 + } + + var self = this + // Add an artificial delay to mimic real world cache latency. + return Q.delay(this._latencyMs) + .then(function actualIncr() { + self._requestCounts.incr += 1 + if (key in self._data) { + self._data[key] += increment + } else { + self._data[key] = increment + } + }) +} + /** @override */ FakeCache.prototype.set = function (key, value, maxAgeMs, setWhenNotExist) { if (this._failureCount > 0) return this._fakeFail('set') diff --git a/lib/InMemoryCache.js b/lib/InMemoryCache.js index 1853281..eccf571 100644 --- a/lib/InMemoryCache.js +++ b/lib/InMemoryCache.js @@ -121,6 +121,20 @@ InMemoryCache.prototype.mget = function (keys) { return Q.resolve(ret) } +/** @override */ +InMemoryCache.prototype.incr = function (key, increment) { + if (increment === undefined) { + increment = 1 + } + + if (key in this._data) { + this._data[key] += increment + } else { + this._data[key] = increment + } + return Q.resolve(null) +} + /** @override */ InMemoryCache.prototype.set = function (key, val, maxAgeMs, setWhenNotExist) { if ((maxAgeMs === undefined || maxAgeMs <= 0) && !this._maxAgeOverride) { diff --git a/lib/MultiplexingCache.js b/lib/MultiplexingCache.js index 1167206..3040eba 100644 --- a/lib/MultiplexingCache.js +++ b/lib/MultiplexingCache.js @@ -152,6 +152,13 @@ MultiplexingCache.prototype.mset = function (items, maxAgeMs, setWhenNotExist) { } +/** @override */ +MultiplexingCache.prototype.incr = function (key, increment) { + this._invalidateKeys([key]) + return this._delegate.incr(key, increment) +} + + /** @override */ MultiplexingCache.prototype.set = function (key, val, maxAgeMs, setWhenNotExist) { this._invalidateKeys([key]) diff --git a/lib/RedisConnection.js b/lib/RedisConnection.js index aac722f..4989489 100644 --- a/lib/RedisConnection.js +++ b/lib/RedisConnection.js @@ -45,6 +45,18 @@ RedisConnection.prototype.isAvailable = function () { return this._isAvailable } +/** @override */ +RedisConnection.prototype.incr = function (key, increment) { + if (increment === undefined) { + increment = 1 + } + + var deferred = Q.defer() + var params = [key, increment] + this._client.incrby(params, this._makeNodeResolverWithTimeout(deferred, 'incrby', 'Redis [incr] key: ' + key)) + return deferred.promise +} + /** @override */ RedisConnection.prototype.set = function (key, val, maxAgeMs, setWhenNotExist) { return this._compress(val) diff --git a/lib/RedundantCacheGroup.js b/lib/RedundantCacheGroup.js index cc0c3c9..a66127c 100644 --- a/lib/RedundantCacheGroup.js +++ b/lib/RedundantCacheGroup.js @@ -179,6 +179,19 @@ RedundantCacheGroup.prototype.mset = function (items, maxAgeMs, setWhenNotExist) .then(returnTrue) } +/** @override */ +RedundantCacheGroup.prototype.incr = function (key, increment) { + var instances = this._getAllInstances() + var promises = [] + + for (var i = 0; i < instances.length; i++) { + promises.push(instances[i].incr(key, increment)) + } + + return Q.all(promises) + .then(returnTrue) +} + /** @override */ RedundantCacheGroup.prototype.set = function (key, val, maxAgeMs, setWhenNotExist) { var instances = this._getAllInstances() diff --git a/package.json b/package.json index 1c41e31..4e76c26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zcache", "description": "AWS zone-aware multi-layer cache", - "version": "0.5.0", + "version": "0.5.1", "homepage": "https://github.com/Medium/zcache", "authors": [ "Jeremy Stanley (https://github.com/azulus)", diff --git a/test/test_InMemoryCache.js b/test/test_InMemoryCache.js index 53ed89e..9b26ba0 100644 --- a/test/test_InMemoryCache.js +++ b/test/test_InMemoryCache.js @@ -25,6 +25,21 @@ exports.testInMemoryCache = function (test) { test.done() } +exports.testCacheIncr = function (test) { + var self = this + test.equal(0, this.cI.getKeyCount(), 'There is no key in cache') + this.cI.incr('counter', 15) + .then(function() { + test.equal(self.cI._data['counter'], 15, '15 should be returned') + test.equal(1, self.cI.getKeyCount(), 'There is 1 key in cache') + return self.cI.incr('counter') + }).then(function() { + test.equal(self.cI._data['counter'], 16, '16 should be returned') + test.equal(1, self.cI.getKeyCount(), 'There is 1 key in cache') + test.done() + }) +} + exports.testCacheSet = function (test) { var self = this test.equal(0, this.cI.getKeyCount(), 'There is no key in cache') diff --git a/test/test_RedisConnection.js b/test/test_RedisConnection.js index 40a7e44..9043c98 100644 --- a/test/test_RedisConnection.js +++ b/test/test_RedisConnection.js @@ -108,6 +108,38 @@ builder.add(function testRedisConnection(test) { cacheInstance.connect() }) +builder.add(function testIncr(test) { + var cacheInstance = new zcache.RedisConnection('localhost', 6379) + + cacheInstance.on('connect', function () { + cacheInstance.removeAllListeners('connect') + test.equal(cacheInstance.isAvailable(), true, 'Connection should be available') + + cacheInstance.incr('counter', 15) + .then(function () { + return cacheInstance.incr('counter') + }) + .then(function (val) { + return cacheInstance.get('counter') + }) + .then(function (val) { + test.equal(val, '16') + cacheInstance.destroy() + }) + .fail(function (e) { + console.error(e) + test.fail(e.message) + test.done() + }) + }) + + cacheInstance.on('destroy', function () { + test.done() + }) + + cacheInstance.connect() +}) + builder.add(function testSetNotExist(test) { var cacheInstance = new zcache.RedisConnection('localhost', 6379)