Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 0d9e48f

Browse files
author
Jan Krems
committed
feat: True timeouts for cache calls
1 parent bbe1b76 commit 0d9e48f

File tree

3 files changed

+95
-2
lines changed

3 files changed

+95
-2
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ The two important ones are `freshFor` and `expire`:
138138

139139
* `expire` is the time in seconds after which a value should be deleted from the cache (or whatever expiring natively means for the backend). Usually you'd want this to be `0` (never expire).
140140
* `freshFor` is the time in seconds after which a value should be replaced. Replacing the value is done in the background and while the new value is generated (e.g. data is fetched from some service) the stale value is returned. Think of `freshFor` as a smarter `expire`.
141+
* `timeout` is the maximum time in milliseconds to wait for cache operations to complete.
142+
Configuring a timeout ensures that all `get` and `set` operations fail fast.
143+
Otherwise there will be situations where one of the cache hosts goes down and reads hang for minutes while the memcached client retries to establish a connection.
144+
It's **highly** recommended to set a timeout.
145+
If `timeout` is left `undefined`, no timeout will be set and the operations will only fail once the underlying client, e.g. [`memcached`](https://github.com/3rd-Eden/memcached), gave up.
141146

142147
### Cache.set(key, value, opts, cb) -> Promise[Value]
143148

lib/cache.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Cache.prototype._set = function _set(key, val, options) {
8787
}, options);
8888
}
8989

90-
return util.toPromise(val).then(writeToBackend);
90+
return this._applyTimeout(util.toPromise(val).then(writeToBackend));
9191
};
9292

9393
Cache.prototype.set = function set(rawKey, val, _opts, _cb) {
@@ -98,8 +98,16 @@ Cache.prototype.set = function set(rawKey, val, _opts, _cb) {
9898
return this._set(key, val, optsWithDefaults).nodeify(args.cb);
9999
};
100100

101+
Cache.prototype._applyTimeout = function _applyTimeout(value) {
102+
var timeoutMs = this.defaults.timeout;
103+
if (timeoutMs > 0) {
104+
return value.timeout(timeoutMs);
105+
}
106+
return value;
107+
};
108+
101109
Cache.prototype._getWrapped = function _getWrapped(key) {
102-
return this.backend.get(key);
110+
return this._applyTimeout(this.backend.get(key));
103111
};
104112
// For backwards compatibility, eventually we should deprecate this.
105113
// It *should* be a private API.

test/timeout.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import assert from 'assertive';
2+
import Bluebird from 'bluebird';
3+
import { identity } from 'lodash';
4+
5+
import Cache from '../lib/cache';
6+
7+
describe('Cache timeouts', () => {
8+
const cache = new Cache({
9+
backend: {
10+
get() {
11+
return Bluebird.resolve({ d: 'get result' }).delay(150);
12+
},
13+
set() {
14+
return Bluebird.resolve('set result').delay(150);
15+
},
16+
},
17+
name: 'awesome-name',
18+
debug: true,
19+
});
20+
21+
describe('with a timeout <150ms', () => {
22+
before(() => cache.defaults.timeout = 50);
23+
24+
it('get fails fast', async () => {
25+
const err = await Bluebird.race([
26+
cache.get('my-key').then(null, identity),
27+
Bluebird.delay(100, 'too slow'), // this should not be used
28+
]);
29+
assert.expect(err instanceof Error);
30+
assert.equal('TimeoutError', err.name);
31+
});
32+
33+
it('set fails fast', async () => {
34+
const err = await Bluebird.race([
35+
cache.set('my-key', 'my-value').then(null, identity),
36+
Bluebird.delay(100, 'too slow'), // this should not be used
37+
]);
38+
assert.expect(err instanceof Error);
39+
assert.equal('TimeoutError', err.name);
40+
});
41+
42+
it('getOrElse fails fast', async () => {
43+
const value = await Bluebird.race([
44+
cache.getOrElse('my-key', 'my-value').then(null, identity),
45+
// We need to add a bit of time here because we'll run into the
46+
// timeout twice - once when trying to read and once while writing.
47+
Bluebird.delay(150, 'too slow'), // this should not be used
48+
]);
49+
assert.equal('my-value', value);
50+
});
51+
});
52+
53+
describe('with a timeout >150ms', () => {
54+
before(() => cache.defaults.timeout = 250);
55+
56+
it('receives the value', async () => {
57+
const value = await Bluebird.race([
58+
cache.get('my-key').then(null, identity),
59+
Bluebird.delay(200, 'too slow'), // this should not be used
60+
]);
61+
assert.equal('get result', value);
62+
});
63+
64+
it('sets the value', async () => {
65+
const value = await Bluebird.race([
66+
cache.set('my-key', 'my-value').then(null, identity),
67+
Bluebird.delay(200, 'too slow'), // this should not be used
68+
]);
69+
assert.equal('set result', value);
70+
});
71+
72+
it('getOrElse can retrieve a value', async () => {
73+
const value = await Bluebird.race([
74+
cache.getOrElse('my-key', 'my-value').then(null, identity),
75+
Bluebird.delay(200, 'too slow'), // this should not be used
76+
]);
77+
assert.equal('get result', value);
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)