Skip to content

Commit 1e08cd6

Browse files
Brian DeteringBrian Detering
authored andcommitted
redis transactions hard prime
1 parent de3a8e6 commit 1e08cd6

File tree

4 files changed

+111
-67
lines changed

4 files changed

+111
-67
lines changed

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Redis Dataloader
22

33
Batching and Caching layer using Redis as the Caching layer.
4-
Redis Dataloader is based on the [Facebook Dataloader](https://github.com/facebook/dataloader),
5-
and uses it internally.
4+
Redis Dataloader wraps [Facebook Dataloader](https://github.com/facebook/dataloader),
5+
adding Redis as a caching layer.
66

77
## Example
88

99
```javascript
10-
const redis = require('redis').createClient();
10+
const redisClient = require('redis').createClient();
1111
const DataLoader = require('dataloader');
12-
const RedisDataLoader = require('redis-dataloader')({ redis: redis });
12+
const RedisDataLoader = require('redis-dataloader')({ redis: redisClient });
1313

1414
const loader = new RedisDataLoader(
1515
// set a prefix for the keys stored in redis. This way you can avoid key
@@ -20,11 +20,16 @@ const loader = new RedisDataLoader(
2020
// The options here are the same as the regular dataloader options, with
2121
// the additional option "expire"
2222
{
23-
// caching here is a local in memory cache
23+
// caching here is a local in memory cache. Caching is always done
24+
// to redis.
2425
cache: true,
2526
// if set redis keys will be set to expire after this many seconds
2627
// this may be useful as a fallback for a redis cache.
27-
expire: 60
28+
expire: 60,
29+
// can include a custom serialization and deserialization for
30+
// storage in redis.
31+
serialize: date => date.getTime(),
32+
deserialize: timestamp => new Date(timestamp)
2833
}
2934
);
3035

@@ -40,9 +45,10 @@ loader.clear(5).then(() => {})
4045
In general, RedisDataLoader has the same API as the Facebook Dataloader Api,
4146
with a few differences. Read through the [Facebook Dataloader documentation](https://github.com/facebook/dataloader) and then note the differences mentioned here.
4247

43-
- `clear` returns a promise (waits until redis succeeds at deleting the key)
48+
- `clear` returns a promise (waits until redis succeeds at deleting the key). Facebook Dataloader's `clear` method is synchronous.
4449
- `clearAll` is not available (redis does not have an efficient way to do this?)
45-
- `prime` will always overwrite the redis cache. It in turn calls prime on the local cache (which does not adjust the cache if the key already exists)
50+
- `prime` will always overwrite the cache. Facebook Dataloader will only write to
51+
its cache if a value is not already present. Prime is asyncronous and returns a Promise.
4652
- dataloader results must be either `null` or a JSON object.
4753

4854
### Instantiation

index.js

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,30 @@ const DataLoader = require('dataloader');
77
module.exports = fig => {
88
const redis = fig.redis;
99

10-
const parse = resp => Q.Promise((resolve, reject) => {
10+
const parse = (resp, opt) => Q.Promise((resolve, reject) => {
1111
try {
12-
resolve(resp !== '' && resp !== null ? JSON.parse(resp) : resp);
12+
if(resp === '' || resp === null) {
13+
resolve(resp);
14+
}
15+
else if(opt.deserialize) {
16+
resolve(opt.deserialize(resp));
17+
}
18+
else {
19+
resolve(JSON.parse(resp));
20+
}
1321
}
1422
catch(err) {
1523
reject(err);
1624
}
1725
});
1826

19-
const toString = val => {
27+
const toString = (val, opt) => {
2028
if(val === null) {
2129
return Q('');
2230
}
31+
else if(opt.serialize) {
32+
return Q(opt.serialize(val));
33+
}
2334
else if(_.isObject(val)) {
2435
return Q(JSON.stringify(val));
2536
}
@@ -28,42 +39,49 @@ module.exports = fig => {
2839
}
2940
};
3041

31-
const rSet = (keySpace, key, rawVal, expire) => toString(rawVal)
32-
.then(val => Q.Promise((resolve, reject) => redis.set(
33-
`${keySpace}:${key}`, val, (err, resp) => {
34-
if(err) {
35-
reject(err);
36-
}
37-
else {
38-
if(expire) {
39-
redis.expire(`${keySpace}:${key}`, expire);
40-
}
41-
resolve(resp);
42-
}
42+
const makeKey = (keySpace, key) => `${keySpace}:${key}`;
43+
44+
const rSetAndGet = (keySpace, key, rawVal, opt) => toString(rawVal, opt)
45+
.then(val => Q.Promise((resolve, reject) => {
46+
const fullKey = makeKey(keySpace, key);
47+
const multi = redis.multi();
48+
multi.set(fullKey, val);
49+
if(opt.expire) {
50+
multi.expire(fullKey, opt.expire);
4351
}
44-
)));
52+
multi.get(fullKey);
53+
multi.exec((err, replies) => err ?
54+
reject(err) : parse(_.last(replies), opt).then(resolve)
55+
);
56+
}));
57+
58+
const rGet = (keySpace, key, opt) => Q.Promise(
59+
(resolve, reject) => redis.get(
60+
makeKey(keySpace, key),
61+
(err, result) => err ? reject(err) : parse(result, opt).then(resolve)
62+
)
63+
);
4564

46-
const rMGet = (keySpace, keys) => {
47-
return Q.Promise((resolve, reject) => redis.mget(
48-
_.map(keys, k => `${keySpace}:${k}`),
65+
const rMGet = (keySpace, keys, opt) => Q.Promise(
66+
(resolve, reject) => redis.mget(
67+
_.map(keys, k => makeKey(keySpace, k)),
4968
(err, results) => err ?
5069
reject(err) :
51-
Q.all(_.map(results, parse)).then(resolve)
52-
));
53-
};
70+
Q.all(_.map(results, r => parse(r, opt))).then(resolve)
71+
)
72+
);
5473

5574
const rDel = (keySpace, key) => Q.Promise((resolve, reject) => redis.del(
56-
`${keySpace}:${key}`, (err, resp) => err ? reject(err) : resolve(resp)
75+
makeKey(keySpace, key), (err, resp) => err ? reject(err) : resolve(resp)
5776
));
5877

5978
return class RedisDataLoader {
60-
constructor(ks, userLoader, options) {
79+
constructor(ks, userLoader, opt) {
80+
const customOptions = ['expire', 'serialize', 'deserialize'];
81+
this.opt = _.pick(opt, customOptions) || {};
6182
this.keySpace = ks;
62-
63-
this.expire = options && options.expire;
64-
6583
this.loader = new DataLoader(
66-
keys => rMGet(this.keySpace, keys)
84+
keys => rMGet(this.keySpace, keys, this.opt)
6785
.then(results => Q.all(_.map(
6886
results,
6987
(v, i) => {
@@ -72,17 +90,17 @@ module.exports = fig => {
7290
}
7391
else if(v === null) {
7492
return userLoader.load(keys[i])
75-
.then(resp => {
76-
return rSet(this.keySpace, keys[i], resp, this.expire)
77-
.then(() => resp);
78-
});
93+
.then(resp => rSetAndGet(
94+
this.keySpace, keys[i], resp, this.opt
95+
))
96+
.then(r => r === '' ? null : r);
7997
}
8098
else {
8199
return Q(v);
82100
}
83101
}
84102
))),
85-
_.omit(options, 'expire')
103+
_.omit(opt, customOptions)
86104
);
87105
}
88106

@@ -95,8 +113,8 @@ module.exports = fig => {
95113
}
96114

97115
prime(key, val) {
98-
return rSet(this.keySpace, key, val, this.expire)
99-
.then(() => this.loader.prime(key, val));
116+
return rSetAndGet(this.keySpace, key, val, this.opt)
117+
.then(resp => this.loader.clear(key).prime(key, resp));
100118
}
101119

102120
clear(key) {

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redis-dataloader",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "DataLoader Using Redis as a Cache",
55
"main": "index.js",
66
"scripts": {
@@ -14,7 +14,8 @@
1414
"DataLoader",
1515
"Cache",
1616
"Redis",
17-
"Batch"
17+
"Batch",
18+
"Facebook"
1819
],
1920
"author": "Brian Detering",
2021
"license": "MIT",

test.js

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,27 +50,6 @@ describe('redis-dataloader', () => {
5050
afterEach(() => _.each(this.stubs, s => s.restore()));
5151

5252
describe('load', () => {
53-
it('should handle redis key expiration if set', done => {
54-
const loader = new RedisDataLoader(
55-
this.keySpace,
56-
this.userLoader(),
57-
{ cache: false, expire: 1 }
58-
);
59-
60-
loader.load('json')
61-
.then(data => {
62-
expect(data).to.deep.equal(this.data.json);
63-
setTimeout(() => {
64-
loader.load('json')
65-
.then(data => {
66-
expect(data).to.deep.equal(this.data.json);
67-
expect(this.loadFn.callCount).to.equal(2);
68-
done();
69-
}).done();
70-
}, 1100);
71-
}).done();
72-
});
73-
7453
it('should load json value', done => {
7554
this.loader.load('json').then(data => {
7655
expect(data).to.deep.equal(this.data.json);
@@ -137,6 +116,45 @@ describe('redis-dataloader', () => {
137116
done();
138117
}).done();
139118
});
119+
120+
it('should handle redis key expiration if set', done => {
121+
const loader = new RedisDataLoader(
122+
this.keySpace,
123+
this.userLoader(),
124+
{ cache: false, expire: 1 }
125+
);
126+
127+
loader.load('json')
128+
.then(data => {
129+
expect(data).to.deep.equal(this.data.json);
130+
setTimeout(() => {
131+
loader.load('json')
132+
.then(data => {
133+
expect(data).to.deep.equal(this.data.json);
134+
expect(this.loadFn.callCount).to.equal(2);
135+
done();
136+
}).done();
137+
}, 1100);
138+
}).done();
139+
});
140+
141+
it('should handle custom serialize and deserialize method', done => {
142+
const loader = new RedisDataLoader(
143+
this.keySpace,
144+
this.userLoader(),
145+
{
146+
serialize: v => 100,
147+
deserialize: v => new Date(Number(v))
148+
}
149+
);
150+
151+
loader.load('json')
152+
.then(data => {
153+
expect(data).to.be.instanceof(Date);
154+
expect(data.getTime()).to.equal(100);
155+
done();
156+
}).done();
157+
});
140158
});
141159

142160
describe('loadMany', () => {
@@ -182,7 +200,8 @@ describe('redis-dataloader', () => {
182200
});
183201

184202
it('should require a key', done => {
185-
this.loader.clear().catch(err => {
203+
this.loader.clear()
204+
.catch(err => {
186205
expect(err.message).to.equal('Key parameter is required');
187206
done();
188207
}).done();

0 commit comments

Comments
 (0)