Skip to content

Commit e5dc325

Browse files
committed
basic client with simple unit test
1 parent ce7fa89 commit e5dc325

4 files changed

Lines changed: 133 additions & 0 deletions

File tree

lib/Config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,11 @@ class Config extends EventEmitter {
18391839

18401840

18411841
if (config.rateLimiting?.enabled) {
1842+
// rate limiting uses the same localCache config defined for S3 to avoid
1843+
// config duplication.
1844+
assert(config.localCache, 'missing required property of rateLimit ' +
1845+
'configuration: localCache');
1846+
18421847
this.rateLimiting.enabled = true;
18431848

18441849
assert.strictEqual(typeof config.rateLimiting.serviceUserArn, 'string');
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const fs = require('fs');
2+
3+
const Redis = require('ioredis');
4+
5+
const { config } = require('../../../Config');
6+
7+
8+
const updateCounterScript = fs.readFileSync(`${__dirname }/updateCounter.lua`).toString();
9+
10+
const SCRIPTS = {
11+
updateCounter: {
12+
numberOfKeys: 1,
13+
lua: updateCounterScript,
14+
},
15+
};
16+
17+
class RateLimitClient {
18+
constructor(redisConfig) {
19+
this.redis = new Redis({
20+
...redisConfig,
21+
scripts: SCRIPTS,
22+
lazyConnect: true,
23+
});
24+
}
25+
26+
getLocalCounter(key, cb) {
27+
this.redis.get(key, cb);
28+
}
29+
30+
updateLocalCounters(batch, cb) {
31+
const pipeline = this.redis.pipeline();
32+
for (const { key, ts, counter } of batch) {
33+
pipeline.updateCounter(key, ts, counter);
34+
}
35+
36+
37+
pipeline.exec((err, results) => {
38+
if (err) {
39+
cb(err);
40+
return;
41+
}
42+
43+
cb(null, results.map(([_, counter], i) => [batch[i].key, counter]));
44+
});
45+
}
46+
}
47+
48+
let counterClient;
49+
if (config.rateLimiting.enabled) {
50+
counterClient = new RateLimitClient(config.localCache);
51+
}
52+
53+
module.exports = {
54+
counterClient,
55+
RateLimitClient
56+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
local counterExists = redis.call('EXISTS', KEYS[1])
2+
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
14+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const assert = require('assert');
2+
3+
const { RateLimitClient } = require('../../../../../lib/api/apiUtils/rateLimit/client');
4+
5+
class RedisStub {
6+
constructor() {
7+
this.data = {}
8+
}
9+
10+
pipeline() {
11+
return new PipelineStub();
12+
}
13+
}
14+
15+
class PipelineStub {
16+
constructor() {
17+
this.ops = [];
18+
}
19+
20+
updateCounter(key, ts, value) {
21+
this.ops.push([key, ts, value]);
22+
}
23+
24+
exec(cb) {
25+
cb(null, this.ops.map(v => [1, v[2]]));
26+
}
27+
}
28+
29+
describe('test RateLimitClient', () => {
30+
let client;
31+
32+
before(() => {
33+
client = new RateLimitClient({});
34+
});
35+
36+
beforeEach(() => {
37+
client.redis = new RedisStub();
38+
});
39+
40+
it('should update a batch of counters', done => {
41+
const ts = Date.now();
42+
const batch = [
43+
{ key: 'foo', ts, counter: ts + 100 },
44+
{ key: 'bar', ts, counter: ts + 200 },
45+
{ key: 'qux', ts, counter: ts + 300 },
46+
]
47+
48+
client.updateLocalCounters(batch, (err, results) => {
49+
assert.ifError(err);
50+
assert.deepStrictEqual(results, [
51+
['foo', ts + 100],
52+
['bar', ts + 200],
53+
['qux', ts + 300],
54+
]);
55+
done();
56+
});
57+
});
58+
});

0 commit comments

Comments
 (0)