Skip to content

Commit 7d124ed

Browse files
author
João Barbosa
committed
Use sorted set to limit with moving window
1 parent 619d624 commit 7d124ed

File tree

3 files changed

+37
-73
lines changed

3 files changed

+37
-73
lines changed

index.js

Lines changed: 22 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
var assert = require('assert');
6+
var microtime = require('./microtime');
67

78
/**
89
* Expose `Limiter`.
@@ -27,7 +28,7 @@ function Limiter(opts) {
2728
assert(this.db, '.db required');
2829
this.max = opts.max || 2500;
2930
this.duration = opts.duration || 3600000;
30-
this.prefix = 'limit:' + this.id + ':';
31+
this.key = 'limit:' + this.id;
3132
}
3233

3334
/**
@@ -47,88 +48,38 @@ Limiter.prototype.inspect = function() {
4748
* Get values and header / status code and invoke `fn(err, info)`.
4849
*
4950
* redis is populated with the following keys
50-
* that expire after N seconds:
51+
* that expire after N milliseconds:
5152
*
52-
* - limit:<id>:count
53-
* - limit:<id>:limit
54-
* - limit:<id>:reset
53+
* - limit:<id>
5554
*
5655
* @param {Function} fn
5756
* @api public
5857
*/
5958

60-
Limiter.prototype.get = function(fn) {
61-
var count = this.prefix + 'count';
62-
var limit = this.prefix + 'limit';
63-
var reset = this.prefix + 'reset';
59+
Limiter.prototype.get = function (fn) {
60+
var key = this.key;
6461
var duration = this.duration;
6562
var max = this.max;
6663
var db = this.db;
67-
68-
function create() {
69-
var ex = (Date.now() + duration) / 1000 | 0;
70-
71-
db.multi()
72-
.set([count, max, 'PX', duration, 'NX'])
73-
.set([limit, max, 'PX', duration, 'NX'])
74-
.set([reset, ex, 'PX', duration, 'NX'])
75-
.exec(function(err, res) {
76-
if (err) return fn(err);
77-
78-
// If the request has failed, it means the values already
79-
// exist in which case we need to get the latest values.
80-
if (isFirstReplyNull(res)) return mget();
81-
82-
fn(null, {
83-
total: max,
84-
remaining: max,
85-
reset: ex
86-
});
87-
});
88-
}
89-
90-
function decr(res) {
91-
var n = ~~res[0];
92-
var max = ~~res[1];
93-
var ex = ~~res[2];
94-
var dateNow = Date.now();
95-
96-
if (n <= 0) return done();
97-
98-
function done() {
99-
fn(null, {
100-
total: max,
101-
remaining: n < 0 ? 0 : n,
102-
reset: ex
103-
});
104-
}
105-
106-
db.multi()
107-
.decr(count)
108-
.pexpire([count, ex * 1000 - dateNow])
109-
.pexpire([limit, ex * 1000 - dateNow])
110-
.pexpire([reset, ex * 1000 - dateNow])
111-
.exec(function(err, res) {
112-
if (err) return fn(err);
113-
if (isFirstReplyNull(res)) return mget();
114-
n = Array.isArray(res[0]) ? ~~res[0][1] : ~~res[0];
115-
done();
116-
});
117-
}
118-
119-
function mget() {
120-
db.watch([count], function(err) {
64+
var now = microtime.now();
65+
var start = now - duration * 1000;
66+
67+
db.multi()
68+
.zremrangebyscore([key, 0, start])
69+
.zcard([key])
70+
.zadd([key, now, now])
71+
.zrange([key, 0, 0])
72+
.pexpire([key, duration])
73+
.exec(function (err, res) {
12174
if (err) return fn(err);
122-
db.mget([count, limit, reset], function(err, res) {
123-
if (err) return fn(err);
124-
if (!res[0] && res[0] !== 0) return create();
125-
126-
decr(res);
75+
var count = parseInt(Array.isArray(res[0]) ? res[1][1] : res[1]);
76+
var oldest = parseInt(Array.isArray(res[0]) ? res[3][1] : res[3]);
77+
fn(null, {
78+
remaining: count < max ? max - count : 0,
79+
reset: Math.floor((oldest + duration) / 1000000),
80+
total: max
12781
});
12882
});
129-
}
130-
131-
mget();
13283
};
13384

13485
/**

microtime.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
var time = Date.now() * 1e3;
3+
var start = process.hrtime();
4+
5+
/**
6+
* Expose `now`.
7+
*/
8+
9+
module.exports.now = function() {
10+
var diff = process.hrtime(start);
11+
12+
return time + diff[0] * 1e6 + Math.round(diff[1] * 1e-3);
13+
}

test/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ var Limiter = require('..'),
212212

213213
if (err) {
214214
done(err);
215-
}
216-
else {
215+
} else {
216+
responses.sort(function (r1, r2) { return r1[1].remaining < r2[1].remaining; });
217217
responses.forEach(function(res) {
218218
res[1].remaining.should.equal(left < 0 ? 0 : left);
219219
left--;

0 commit comments

Comments
 (0)