Skip to content

Commit 9ec7d56

Browse files
committed
rework lua script and add basic functional test
1 parent e5dc325 commit 9ec7d56

4 files changed

Lines changed: 104 additions & 29 deletions

File tree

lib/api/apiUtils/rateLimit/client.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,47 @@ class RateLimitClient {
2323
});
2424
}
2525

26-
getLocalCounter(key, cb) {
27-
this.redis.get(key, cb);
28-
}
26+
/**
27+
* @typedef {Object} CounterUpdateBatch
28+
* @property {string} key - counter key
29+
* @property {number} cost - cost to add to counter
30+
*/
31+
32+
/**
33+
* @typedef {Object} CounterUpdateBatchResult
34+
* @property {string} key - counter key
35+
* @property {number} value - current value of counter
36+
*/
2937

38+
/**
39+
* @callback RateLimitClient~batchUpdate
40+
* @param {Error} err
41+
* @param {undefined|CounterUpdateBatchResult[]}
42+
*/
43+
44+
/**
45+
* Add cost to the counter at key.
46+
* Returns the new value for the counter
47+
*
48+
* @param {CounterUpdateBatch[]} batch - batch of counter updates
49+
* @param {RateLimitClient~batchUpdate} cb
50+
*/
3051
updateLocalCounters(batch, cb) {
3152
const pipeline = this.redis.pipeline();
32-
for (const { key, ts, counter } of batch) {
33-
pipeline.updateCounter(key, ts, counter);
53+
for (const { key, cost } of batch) {
54+
pipeline.updateCounter(key, cost);
3455
}
3556

36-
3757
pipeline.exec((err, results) => {
3858
if (err) {
3959
cb(err);
4060
return;
4161
}
4262

43-
cb(null, results.map(([_, counter], i) => [batch[i].key, counter]));
63+
cb(null, results.map(([_, value], i) => ({
64+
key: batch[i].key,
65+
value,
66+
})));
4467
});
4568
}
4669
}
Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
local counterExists = redis.call('EXISTS', KEYS[1])
1+
local ts = redis.call('TIME')
2+
local currentTime = ts[1] * 1000
3+
currentTime = currentTime + math.floor(ts[2] / 1000)
4+
5+
local newValue = currentTime + tonumber(ARGV[1])
26

3-
if counterExists ~= 1 then
4-
redis.call('SET', KEYS[1], ARGV[2])
5-
return ARGV[2]
6-
else
7-
local currentValue = redis.call('GET', KEYS[1])
8-
local localDelay = currentValue - ARGV[1]
9-
local remoteDelay = ARGV[2] - ARGV[1]
10-
local newCounter = localDelay + remoteDelay + ARGV[1]
11-
redis.call('SET', KEYS[1], newCounter)
12-
redis.call('EXPIREAT', KEYS[1], newCounter)
13-
return newCounter
7+
local counterExists = redis.call('EXISTS', KEYS[1])
8+
if counterExists == 1 then
9+
local currentValue = tonumber(redis.call('GET', KEYS[1]))
10+
if currentValue > currentTime then
11+
local currentCost = currentValue - currentTime
12+
newValue = newValue + currentCost
13+
end
1414
end
15+
16+
redis.call('SET', KEYS[1], newValue)
17+
18+
local expiry = math.ceil(newValue / 1000)
19+
redis.call('EXPIREAT', KEYS[1], expiry)
20+
21+
return newValue
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const assert = require('assert');
2+
3+
const { config } = require('../../../../../lib/Config');
4+
const { RateLimitClient } = require('../../../../../lib/api/apiUtils/rateLimit/client');
5+
6+
7+
const counterKey = 'foo';
8+
9+
describe('Test RateLimitClient', () => {
10+
let client;
11+
12+
before(done => {
13+
client = new RateLimitClient(config.localCache);
14+
client.redis.connect(done);
15+
});
16+
17+
beforeEach(done => {
18+
client.redis.del(counterKey, err => done(err));
19+
});
20+
21+
it('should set the value of an empty counter', done => {
22+
const batch = [{ key: counterKey, cost: 10000 }];
23+
client.updateLocalCounters(batch, (err, res) => {
24+
assert.ifError(err);
25+
assert.strictEqual(res.length, 1);
26+
assert.strictEqual(res[0].key, counterKey);
27+
done();
28+
});
29+
});
30+
31+
it('should increment the value of an existing counter', done => {
32+
const batch = [{ key: counterKey, cost: 10000 }];
33+
client.updateLocalCounters(batch, (err, res) => {
34+
assert.ifError(err);
35+
console.log({ res });
36+
const { value: existingValue } = res[0];
37+
client.updateLocalCounters(batch, (err, res) => {
38+
assert.ifError(err);
39+
console.log({ res });
40+
const { value: newValue } = res[0];
41+
assert(newValue > existingValue, `${newValue} is not greater than ${existingValue}`);
42+
done();
43+
});
44+
});
45+
});
46+
});

tests/unit/api/apiUtils/rateLimit/client.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ class PipelineStub {
1717
this.ops = [];
1818
}
1919

20-
updateCounter(key, ts, value) {
21-
this.ops.push([key, ts, value]);
20+
updateCounter(key, cost) {
21+
this.ops.push([key, cost]);
2222
}
2323

2424
exec(cb) {
25-
cb(null, this.ops.map(v => [1, v[2]]));
25+
cb(null, this.ops.map(v => [1, v[1]]));
2626
}
2727
}
2828

@@ -38,19 +38,18 @@ describe('test RateLimitClient', () => {
3838
});
3939

4040
it('should update a batch of counters', done => {
41-
const ts = Date.now();
4241
const batch = [
43-
{ key: 'foo', ts, counter: ts + 100 },
44-
{ key: 'bar', ts, counter: ts + 200 },
45-
{ key: 'qux', ts, counter: ts + 300 },
42+
{ key: 'foo', cost: 100 },
43+
{ key: 'bar', cost: 200 },
44+
{ key: 'qux', cost: 300 },
4645
]
4746

4847
client.updateLocalCounters(batch, (err, results) => {
4948
assert.ifError(err);
5049
assert.deepStrictEqual(results, [
51-
['foo', ts + 100],
52-
['bar', ts + 200],
53-
['qux', ts + 300],
50+
{ key: 'foo', value: 100 },
51+
{ key: 'bar', value: 200 },
52+
{ key: 'qux', value: 300 },
5453
]);
5554
done();
5655
});

0 commit comments

Comments
 (0)