-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: improve caching - adds TTL, cacheTransport and cacheGetKey (#739)
* feat: add cacheTTL option * feat: add `cacheGetKey` and `cacheTransport` options * refactor: move default cache implementation to the separate file * test: cache tests --------- Signed-off-by: Ilya Amelevich <ilya.amelevich@gmail.com>
- Loading branch information
1 parent
0aecea7
commit dc9be53
Showing
4 changed files
with
277 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* Simple in-memory cache implementation | ||
* @class MemoryCache | ||
* @property {Map} cache Cache map | ||
*/ | ||
class MemoryCache { | ||
constructor () { | ||
this.cache = new Map(); | ||
} | ||
|
||
/** | ||
* Get cache value by key | ||
* @param {string} key Cache key | ||
* @return {any} Response from cache | ||
*/ | ||
get (key) { | ||
const cached = this.cache.get(key); | ||
if (cached) { | ||
if (cached.expiresAt > Date.now() || cached.expiresAt === 0) { | ||
return cached.value; | ||
} | ||
this.cache.delete(key); | ||
} | ||
return undefined; | ||
} | ||
|
||
/** | ||
* Set cache key with value and ttl | ||
* @param {string} key Cache key | ||
* @param {any} value Value to cache | ||
* @param {number} ttl Time to live in milliseconds | ||
* @return {void} | ||
*/ | ||
set (key, value, ttl) { | ||
this.cache.set(key, { | ||
expiresAt: ttl, | ||
value | ||
}); | ||
} | ||
|
||
/** | ||
* Clear cache | ||
* @returns {void} | ||
*/ | ||
flush () { | ||
this.cache.clear(); | ||
} | ||
} | ||
|
||
module.exports = exports = MemoryCache; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
'use strict'; | ||
|
||
const test = require('tape'); | ||
const CircuitBreaker = require('../'); | ||
const common = require('./common'); | ||
|
||
const passFail = common.passFail; | ||
|
||
test('Using cache', t => { | ||
t.plan(9); | ||
const expected = 34; | ||
const options = { | ||
cache: true | ||
}; | ||
const breaker = new CircuitBreaker(passFail, options); | ||
|
||
breaker.fire(expected) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 0, 'does not hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'emits a cacheMiss'); | ||
t.equals(stats.fires, 1, 'fired once'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 1, 'hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'did not emit miss'); | ||
t.equals(stats.fires, 2, 'fired twice'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
breaker.clearCache(); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.shutdown()) | ||
.then(t.end) | ||
.catch(t.fail); | ||
}); | ||
|
||
test('Using cache with TTL', t => { | ||
t.plan(12); | ||
const expected = 34; | ||
const options = { | ||
cache: true, | ||
cacheTTL: 100 | ||
}; | ||
const breaker = new CircuitBreaker(passFail, options); | ||
|
||
return breaker.fire(expected) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 0, 'does not hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'emits a cacheMiss'); | ||
t.equals(stats.fires, 1, 'fired once'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 1, 'hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'did not emit miss'); | ||
t.equals(stats.fires, 2, 'fired twice'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
// wait 100ms for the cache to expire | ||
.then(() => new Promise(resolve => setTimeout(resolve, 100))) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 1, 'hit the cache'); | ||
t.equals(stats.cacheMisses, 2, 'did not emit miss'); | ||
t.equals(stats.fires, 3, 'fired twice'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(t.end) | ||
.catch(t.fail); | ||
}); | ||
|
||
test('Using cache with custom get cache key', t => { | ||
t.plan(9); | ||
const expected = 34; | ||
const options = { | ||
cache: true, | ||
cacheGetKey: x => `key-${x}` | ||
}; | ||
const breaker = new CircuitBreaker(passFail, options); | ||
|
||
breaker.fire(expected) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 0, 'does not hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'emits a cacheMiss'); | ||
t.equals(stats.fires, 1, 'fired once'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 1, 'hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'did not emit miss'); | ||
t.equals(stats.fires, 2, 'fired twice'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
breaker.clearCache(); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.shutdown()) | ||
.then(t.end) | ||
.catch(t.fail); | ||
}); | ||
|
||
test('Using cache with custom transport', t => { | ||
t.plan(9); | ||
const expected = 34; | ||
const cache = new Map(); | ||
const options = { | ||
cache: true, | ||
cacheTransport: { | ||
get: key => cache.get(key), | ||
set: (key, value) => cache.set(key, value), | ||
flush: () => cache.clear() | ||
} | ||
}; | ||
const breaker = new CircuitBreaker(passFail, options); | ||
|
||
breaker.fire(expected) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 0, 'does not hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'emits a cacheMiss'); | ||
t.equals(stats.fires, 1, 'fired once'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(stats.cacheHits, 1, 'hit the cache'); | ||
t.equals(stats.cacheMisses, 1, 'did not emit miss'); | ||
t.equals(stats.fires, 2, 'fired twice'); | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
breaker.clearCache(); | ||
}) | ||
.then(() => breaker.fire(expected)) | ||
.then(arg => { | ||
const stats = breaker.status.stats; | ||
t.equals(arg, expected, | ||
`cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); | ||
}) | ||
.then(() => breaker.shutdown()) | ||
.then(t.end) | ||
.catch(t.fail); | ||
}); |
Oops, something went wrong.