@withjoy/limiter 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -11
- package/limiter.js +81 -25
- package/package.json +4 -6
- package/tests/limiter.test.js +237 -179
- package/tests/performTestWithTestPerMinute.js +31 -0
- package/tests/sanityCheck.js +12 -7
- package/tests/limitd.config +0 -14
package/README.md
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
# limiter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A wrapper for limitd-redis.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Why ?
|
|
4
7
|
|
|
5
8
|
Provides utilities and rollback functionality for our clients.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
This module was originally built to work with `limitd`.
|
|
11
|
+
The prior solution -- and most of the documentation surrounding our standard rate-limiting Buckets --
|
|
12
|
+
can be found in [joylife/limitd](https://bitbucket.org/joylife/limitd/src/master/).
|
|
13
|
+
That solution is now **deprecated**.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Buckets
|
|
17
|
+
|
|
18
|
+
Each client must define the set of rate-limit Buckets that intends to use.
|
|
19
|
+
|
|
20
|
+
The set of `defaultBuckets` defines the standard set of ones that we've historically determined that we need.
|
|
21
|
+
[This list](https://github.com/joylifeinc/limiter/blob/4183c8ca0f5db7392846dc291e481dd307f985f7/limiter.js#L5-L16) may be old, but that's the general idea.
|
|
22
|
+
Your client can use any subset of these Buckets, but when it does, the Best Practice is to use the exact definition(s) here.
|
|
23
|
+
|
|
24
|
+
However, for flexibility reasons, this is not strictly enforced.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Testing:
|
|
8
28
|
|
|
9
29
|
`npm run test` will run offline tests using a mock.
|
|
10
|
-
`npm run sanity-check` will run against a limitd service configured to run with the
|
|
30
|
+
`npm run sanity-check` will run against a limitd service configured to run with the test buckets.
|
|
11
31
|
|
|
12
|
-
##### How can I run that limitd instance quickly ?
|
|
13
32
|
|
|
14
|
-
|
|
33
|
+
## Quick Test
|
|
15
34
|
|
|
16
|
-
run this in the root of this repo:
|
|
17
35
|
```bash
|
|
18
|
-
|
|
19
|
-
--mount \
|
|
20
|
-
type=bind,source="$(pwd)"/tests/limitd.config,target=/etc/limitd.config \
|
|
21
|
-
-it joylife/limitd:release-6-d5e4805
|
|
36
|
+
node tests/performTestWithTestPerMinute.js
|
|
22
37
|
```
|
package/limiter.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
var LimitdRedis = require("limitd-redis"); // name to redis matter
|
|
4
|
+
|
|
5
|
+
const defaultBuckets = {
|
|
6
|
+
emailsByEventId: { size: 3000 },
|
|
7
|
+
emailsByEventIdRate: { size: 500, per_hour: 500 },
|
|
8
|
+
emailsByEventId_Warn: { size: 1000 },
|
|
9
|
+
emailsByEventIdRate_Warn: { size: 200, per_hour: 200 },
|
|
10
|
+
eventsByUserId: { size: 5, per_hour: 5 },
|
|
11
|
+
eventsByIpRate: { size: 240, per_hour: 240 },
|
|
12
|
+
paymentAttempts: { size: 9000 },
|
|
13
|
+
paymentAttemptsByReceiverId: { size: 900 },
|
|
14
|
+
// for development testing
|
|
15
|
+
testPerMinute: { size: 3, per_minute: 1 },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const defaultRedisKeyPrefix = "limitdRedis:";
|
|
5
19
|
|
|
6
20
|
class Operation {
|
|
7
|
-
constructor({opname,type,id,amount}){
|
|
21
|
+
constructor({ opname, type, id, amount }) {
|
|
8
22
|
this.opname = opname;
|
|
9
23
|
this.type = type;
|
|
10
24
|
this.id = id;
|
|
@@ -24,12 +38,14 @@ class Operation {
|
|
|
24
38
|
let connection = null;
|
|
25
39
|
|
|
26
40
|
class Limiter {
|
|
41
|
+
// static defaultBuckets;
|
|
42
|
+
// static pickDefaultBuckets(keys) { ... };
|
|
27
43
|
|
|
28
44
|
constructor(config) {
|
|
29
45
|
this._console = config.console;
|
|
46
|
+
this._config = config;
|
|
30
47
|
this._operationList = [];
|
|
31
|
-
this.
|
|
32
|
-
this.connect(config.limitdUrl);
|
|
48
|
+
this._limitdRedis = this.connect();
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
/*
|
|
@@ -37,11 +53,21 @@ class Limiter {
|
|
|
37
53
|
and we invoke this method from the Constructor.
|
|
38
54
|
stub it to prevent a real connect from happening under Test Suite Conditions
|
|
39
55
|
*/
|
|
40
|
-
connect(
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
connect() {
|
|
57
|
+
let buckets = this._config.buckets || {};
|
|
58
|
+
buckets = {
|
|
59
|
+
...defaultBuckets,
|
|
60
|
+
...buckets,
|
|
61
|
+
};
|
|
62
|
+
const keyPrefix = this._config.keyPrefix || defaultRedisKeyPrefix;
|
|
63
|
+
if (!connection) {
|
|
64
|
+
connection = new LimitdRedis({
|
|
65
|
+
uri: this._config.limitdUrl,
|
|
66
|
+
buckets,
|
|
67
|
+
keyPrefix,
|
|
68
|
+
});
|
|
43
69
|
}
|
|
44
|
-
this.
|
|
70
|
+
this._limitdRedis = connection;
|
|
45
71
|
return connection;
|
|
46
72
|
}
|
|
47
73
|
|
|
@@ -49,16 +75,16 @@ class Limiter {
|
|
|
49
75
|
Go through the log of things we did and do the opposite
|
|
50
76
|
*/
|
|
51
77
|
revert() {
|
|
52
|
-
let ops = this._operationList.map(op => op.clone());
|
|
78
|
+
let ops = this._operationList.map((op) => op.clone());
|
|
53
79
|
this._operationList = [];
|
|
54
80
|
|
|
55
81
|
let p = Promise.resolve();
|
|
56
82
|
for (let op of ops) {
|
|
57
83
|
switch (op.opname) {
|
|
58
|
-
case
|
|
84
|
+
case "put":
|
|
59
85
|
p = p.then(() => this._take(op.type, op.id, op.amount));
|
|
60
86
|
break;
|
|
61
|
-
case
|
|
87
|
+
case "take":
|
|
62
88
|
p = p.then(() => this._put(op.type, op.id, op.amount));
|
|
63
89
|
break;
|
|
64
90
|
}
|
|
@@ -71,24 +97,36 @@ class Limiter {
|
|
|
71
97
|
this._operationList.push(op);
|
|
72
98
|
}
|
|
73
99
|
|
|
100
|
+
close() {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
this._limitdRedis.close((err, resp) => {
|
|
103
|
+
if (err) {
|
|
104
|
+
this._console.error(err);
|
|
105
|
+
return reject(err);
|
|
106
|
+
}
|
|
107
|
+
console.log("Connection closed");
|
|
108
|
+
resolve(resp);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
74
112
|
// promise wrapper for limitd lib put
|
|
75
|
-
_put
|
|
113
|
+
_put(type, id, amount) {
|
|
76
114
|
return new Promise((resolve, reject) =>
|
|
77
|
-
this.
|
|
115
|
+
this._limitdRedis.put(type, id, amount, (err, resp) => {
|
|
78
116
|
if (err) {
|
|
79
117
|
this._console.error(err);
|
|
80
118
|
return reject(err);
|
|
81
119
|
}
|
|
82
|
-
this.record(new Operation({opname: "put",type,id,amount}));
|
|
83
|
-
resolve(
|
|
120
|
+
this.record(new Operation({ opname: "put", type, id, amount }));
|
|
121
|
+
resolve(resp);
|
|
84
122
|
})
|
|
85
123
|
);
|
|
86
124
|
}
|
|
87
125
|
|
|
88
126
|
// promise wrapper for limitd lib take
|
|
89
|
-
_take
|
|
127
|
+
_take(type, id, amount) {
|
|
90
128
|
return new Promise((resolve, reject) =>
|
|
91
|
-
this.
|
|
129
|
+
this._limitdRedis.take(type, id, amount, (err, resp) => {
|
|
92
130
|
if (err) {
|
|
93
131
|
this._console.error(err);
|
|
94
132
|
return reject(err);
|
|
@@ -99,10 +137,10 @@ class Limiter {
|
|
|
99
137
|
return reject(new Error("Non Conformant"));
|
|
100
138
|
}
|
|
101
139
|
|
|
102
|
-
this.record(new Operation({opname: "take",type,id,amount}));
|
|
103
|
-
return resolve(
|
|
140
|
+
this.record(new Operation({ opname: "take", type, id, amount }));
|
|
141
|
+
return resolve(resp);
|
|
104
142
|
})
|
|
105
|
-
)
|
|
143
|
+
);
|
|
106
144
|
}
|
|
107
145
|
|
|
108
146
|
transfer(type, id, amount) {
|
|
@@ -112,17 +150,35 @@ class Limiter {
|
|
|
112
150
|
|
|
113
151
|
this._console.info(`Transfering ${amount} from ${type}-${id}`);
|
|
114
152
|
// If amount == 0, there is nothing to do
|
|
115
|
-
if (amount == 0) {
|
|
116
|
-
|
|
153
|
+
if (amount == 0) {
|
|
154
|
+
return Promise.resolve(null);
|
|
155
|
+
}
|
|
156
|
+
if (amount < 0) {
|
|
157
|
+
return this._put(type, id, -amount);
|
|
158
|
+
}
|
|
117
159
|
return this._take(type, id, amount);
|
|
118
160
|
}
|
|
119
161
|
|
|
120
162
|
transferAll(arr) {
|
|
121
163
|
return Promise.all(
|
|
122
|
-
arr.map(spec => this.transfer(spec[0], spec[1], spec[2]))
|
|
123
|
-
)
|
|
164
|
+
arr.map((spec) => this.transfer(spec[0], spec[1], spec[2]))
|
|
165
|
+
);
|
|
124
166
|
}
|
|
125
167
|
}
|
|
126
168
|
|
|
169
|
+
// 'Class static' syntax in Node 10 is not mature
|
|
170
|
+
// and we don't want to upgrade Node at this point due to backwards compat
|
|
171
|
+
Limiter.defaultBuckets = defaultBuckets;
|
|
172
|
+
Limiter.pickDefaultBuckets = function pickDefaultBuckets(keyArray) {
|
|
173
|
+
const buckets = {};
|
|
174
|
+
for (const key of keyArray) {
|
|
175
|
+
const bucket = defaultBuckets[key];
|
|
176
|
+
if (! bucket) {
|
|
177
|
+
throw new Error(`Limiter.pickDefaultBuckets: no such bucket; "${key}"`);
|
|
178
|
+
}
|
|
179
|
+
buckets[key] = defaultBuckets[key];
|
|
180
|
+
}
|
|
181
|
+
return buckets;
|
|
182
|
+
}
|
|
127
183
|
|
|
128
184
|
module.exports = Limiter;
|
package/package.json
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withjoy/limiter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Api Rate limiter",
|
|
5
5
|
"main": "limiter.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "jest",
|
|
7
|
+
"test": "NODE_ENV=test node --expose-gc --max-old-space-size=6144 node_modules/.bin/jest --logHeapUsage -i --forceExit",
|
|
8
8
|
"sanity-check": "node tests/sanityCheck.js k 10"
|
|
9
9
|
},
|
|
10
10
|
"author": "services@withjoy.com",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"limitd-
|
|
13
|
+
"limitd-redis": "^5.1.1"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"jest": "^24.9.0"
|
|
17
|
-
"limitd": "^5.3.0",
|
|
18
|
-
"limitdctl": "^1.1.1"
|
|
16
|
+
"jest": "^24.9.0"
|
|
19
17
|
},
|
|
20
18
|
"directories": {
|
|
21
19
|
"test": "tests"
|
package/tests/limiter.test.js
CHANGED
|
@@ -1,212 +1,270 @@
|
|
|
1
|
-
const Limiter = require(
|
|
1
|
+
const Limiter = require("../limiter");
|
|
2
2
|
|
|
3
3
|
const PUT_FAILED = "Failed to put!";
|
|
4
4
|
const TAKE_FAILED = "Failed to take!";
|
|
5
|
+
|
|
6
|
+
const TEST_BUCKETS = {
|
|
7
|
+
emails: {
|
|
8
|
+
size: 10,
|
|
9
|
+
},
|
|
10
|
+
emailRate: {
|
|
11
|
+
size: 10,
|
|
12
|
+
per_hour: 10,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
5
15
|
/*
|
|
6
16
|
The limitd service is essentially a token bank
|
|
7
17
|
that we can withdraw from (take) or deposit to (put)
|
|
8
18
|
*/
|
|
9
19
|
const limiterWithMockService = (succeeds = true, conforms = true) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
20
|
+
let token_pile = 0;
|
|
21
|
+
const limiter = new Limiter({
|
|
22
|
+
console: console,
|
|
23
|
+
limitdUrl: "localhost:6379",
|
|
24
|
+
buckets: TEST_BUCKETS,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
limiter._limitdRedis = new (class MockLimitd {
|
|
28
|
+
constructor(should_succeed, should_conform) {
|
|
29
|
+
this.should_succeed = should_succeed;
|
|
30
|
+
this.should_conform = should_conform;
|
|
31
|
+
}
|
|
32
|
+
put(type, id, amount, callback) {
|
|
33
|
+
console.log("called put", type, id, amount);
|
|
34
|
+
console.log(this.should_succeed);
|
|
35
|
+
if (!this.should_succeed) {
|
|
36
|
+
return callback(new Error(PUT_FAILED), "This is not falsey");
|
|
37
|
+
}
|
|
38
|
+
token_pile += amount;
|
|
39
|
+
callback(null, {});
|
|
40
|
+
}
|
|
41
|
+
take(type, id, amount, callback) {
|
|
42
|
+
console.log("called take", type, id, amount);
|
|
43
|
+
if (!this.should_succeed) {
|
|
44
|
+
return callback(new Error(TAKE_FAILED), null);
|
|
45
|
+
} else if (!this.should_conform) {
|
|
46
|
+
return callback(null, {}); // non coformant
|
|
47
|
+
}
|
|
48
|
+
token_pile -= amount;
|
|
49
|
+
callback(null, { conformant: true });
|
|
50
|
+
}
|
|
51
|
+
_num_tokens() {
|
|
52
|
+
return token_pile;
|
|
53
|
+
}
|
|
54
|
+
})(succeeds, conforms);
|
|
55
|
+
return limiter;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
test("perfrom limitd-redis testing with test bucket", async (done) => {
|
|
59
|
+
const limiter = new Limiter({
|
|
60
|
+
limitdUrl: "localhost:6379",
|
|
61
|
+
console: console,
|
|
62
|
+
buckets: {
|
|
63
|
+
test: { size: 3, per_second: 2 },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
// take 2 # Conformant - Remaining: 1
|
|
67
|
+
const result1 = await limiter.transfer("test", "test-key", 2);
|
|
68
|
+
expect(result1).toMatchObject({
|
|
69
|
+
conformant: true,
|
|
70
|
+
remaining: 1,
|
|
71
|
+
limit: 3,
|
|
72
|
+
delayed: false,
|
|
73
|
+
});
|
|
74
|
+
await expect(limiter.transfer("test", "test-key", 2)).rejects.toThrow(
|
|
75
|
+
"Non Conformant"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Put 1
|
|
79
|
+
const result3 = await limiter.transfer("test", "test-key", -1);
|
|
80
|
+
expect(result3).toMatchObject({
|
|
81
|
+
remaining: 2,
|
|
82
|
+
limit: 3,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
//Now Conformant - Remaining: 2
|
|
86
|
+
const result4 = await limiter.transfer("test", "test-key", 2);
|
|
87
|
+
expect(result4).toMatchObject({
|
|
88
|
+
conformant: true,
|
|
89
|
+
remaining: 0,
|
|
90
|
+
limit: 3,
|
|
91
|
+
delayed: false,
|
|
92
|
+
});
|
|
93
|
+
await limiter.close();
|
|
94
|
+
done();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("Resolves to null if we aren't doing a real transfer", (done) => {
|
|
98
|
+
let limiter = limiterWithMockService();
|
|
99
|
+
limiter.transfer("a", "b", 0, null).then((result) => {
|
|
100
|
+
expect(result).toBe(null);
|
|
101
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
102
|
+
done();
|
|
103
|
+
});
|
|
50
104
|
});
|
|
51
105
|
|
|
52
|
-
test("Adds tokens if provided a negative token amount", done => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
106
|
+
test("Adds tokens if provided a negative token amount", (done) => {
|
|
107
|
+
let limiter = limiterWithMockService();
|
|
108
|
+
limiter.transfer("a", "b", -5).then((result) => {
|
|
109
|
+
expect(result).toMatchObject({});
|
|
110
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
111
|
+
done();
|
|
112
|
+
});
|
|
60
113
|
});
|
|
61
114
|
|
|
62
|
-
test("Removes tokens if provided a positive token amount", done => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
115
|
+
test("Removes tokens if provided a positive token amount", (done) => {
|
|
116
|
+
let limiter = limiterWithMockService();
|
|
117
|
+
limiter.transfer("a", "b", 5).then((result) => {
|
|
118
|
+
expect(result).toMatchObject({ conformant: true });
|
|
119
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(-5);
|
|
120
|
+
done();
|
|
121
|
+
});
|
|
70
122
|
});
|
|
71
123
|
|
|
72
|
-
test("Reverts all taken tokens when revert is called", done => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
124
|
+
test("Reverts all taken tokens when revert is called", (done) => {
|
|
125
|
+
let limiter = limiterWithMockService();
|
|
126
|
+
limiter
|
|
127
|
+
.transfer("a", "b", 5)
|
|
128
|
+
.then((result) => {
|
|
129
|
+
expect(result).toMatchObject({ conformant: true });
|
|
130
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(-5);
|
|
131
|
+
})
|
|
132
|
+
.then(() => limiter.revert())
|
|
133
|
+
.then(() => {
|
|
134
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
135
|
+
done();
|
|
136
|
+
});
|
|
84
137
|
});
|
|
85
138
|
|
|
86
|
-
test("Reverts all given tokens when revert is called", done => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
test("Reverts all given tokens when revert is called", (done) => {
|
|
140
|
+
let limiter = limiterWithMockService();
|
|
141
|
+
limiter
|
|
142
|
+
.transfer("a", "b", -5)
|
|
143
|
+
.then((result) => {
|
|
144
|
+
expect(result).toMatchObject({});
|
|
145
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
146
|
+
})
|
|
147
|
+
.then(() => limiter.revert())
|
|
148
|
+
.then(() => {
|
|
149
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
150
|
+
done();
|
|
151
|
+
});
|
|
98
152
|
});
|
|
99
153
|
|
|
100
|
-
test("Reverts a series of actions when revert is called", done => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
154
|
+
test("Reverts a series of actions when revert is called", (done) => {
|
|
155
|
+
let limiter = limiterWithMockService();
|
|
156
|
+
limiter
|
|
157
|
+
.transfer("a", "b", -5)
|
|
158
|
+
.then((result) => {
|
|
159
|
+
expect(result).toMatchObject({});
|
|
160
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
161
|
+
})
|
|
162
|
+
.then(() => limiter.transfer("a", "b", 2))
|
|
163
|
+
.then((result) => {
|
|
164
|
+
expect(result).toMatchObject({ conformant: true });
|
|
165
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(3);
|
|
166
|
+
})
|
|
167
|
+
.then(() => limiter.revert())
|
|
168
|
+
.then(() => {
|
|
169
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
170
|
+
done();
|
|
171
|
+
});
|
|
117
172
|
});
|
|
118
173
|
|
|
119
|
-
test("Properly executes a transferAll()", done => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
test("Properly executes a transferAll()", (done) => {
|
|
175
|
+
let limiter = limiterWithMockService();
|
|
176
|
+
limiter
|
|
177
|
+
.transferAll([
|
|
178
|
+
["a", "b", -5],
|
|
179
|
+
["a", "b", 2],
|
|
180
|
+
])
|
|
181
|
+
.then((result) => {
|
|
182
|
+
expect(result).toMatchObject([{}, { conformant: true }]);
|
|
183
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(3);
|
|
184
|
+
})
|
|
185
|
+
.then(() => limiter.revert())
|
|
186
|
+
.then(() => {
|
|
187
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
188
|
+
done();
|
|
189
|
+
});
|
|
134
190
|
});
|
|
135
191
|
|
|
136
192
|
test("Errors from the underlying service are propagated (put)", () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
let limiter = limiterWithMockService((succeeds = false));
|
|
194
|
+
let endsInFailure = limiter.transfer("a", "b", -5);
|
|
195
|
+
return expect(endsInFailure).rejects.toThrow(PUT_FAILED);
|
|
140
196
|
});
|
|
141
197
|
|
|
142
198
|
test("Errors from the underlying service are propagated (take)", () => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
199
|
+
let limiter = limiterWithMockService((succeeds = false));
|
|
200
|
+
let endsInFailure = limiter.transfer("a", "b", 5);
|
|
201
|
+
return expect(endsInFailure).rejects.toThrow(TAKE_FAILED);
|
|
146
202
|
});
|
|
147
203
|
|
|
148
204
|
test("Non-conforming response results in an error", () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
let limiter = limiterWithMockService((succeeds = true), (conforms = false));
|
|
206
|
+
let endsInFailure = limiter.transfer("a", "b", 5);
|
|
207
|
+
return expect(endsInFailure).rejects.toThrow("Non Conformant");
|
|
152
208
|
});
|
|
153
209
|
|
|
154
|
-
test("An operation that fails is not logged", () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
test("An operation that fails is not logged", (done) => {
|
|
211
|
+
let limiter = limiterWithMockService();
|
|
212
|
+
limiter
|
|
213
|
+
.transfer("emailRate", "b", -5)
|
|
214
|
+
.then((result) => {
|
|
215
|
+
expect(result).toBe(null);
|
|
216
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
217
|
+
limiter.succeeds = false; // somebody is playing games
|
|
218
|
+
})
|
|
219
|
+
.then(() => limiter.transfer("emailRate", "b", 2))
|
|
220
|
+
.catch(() => "failed uniquely!") // do nothing!
|
|
221
|
+
.then((result) => {
|
|
222
|
+
expect(result).toBe("failed uniquely!");
|
|
223
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
224
|
+
limiter.succeeds = true; // for real this time
|
|
225
|
+
})
|
|
226
|
+
.then(() => limiter.revert())
|
|
227
|
+
.then(() => {
|
|
228
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
229
|
+
done();
|
|
230
|
+
});
|
|
174
231
|
});
|
|
175
232
|
|
|
176
|
-
test("Properly handles spamming revert() calls", done => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
233
|
+
test("Properly handles spamming revert() calls", (done) => {
|
|
234
|
+
let limiter = limiterWithMockService();
|
|
235
|
+
limiter
|
|
236
|
+
.transfer("a", "b", -5)
|
|
237
|
+
.then((result) => {
|
|
238
|
+
expect(result).toMatchObject({});
|
|
239
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(5);
|
|
240
|
+
})
|
|
241
|
+
.then(() => limiter.transfer("a", "b", 2))
|
|
242
|
+
.then((result) => {
|
|
243
|
+
expect(result).toMatchObject({ conformant: true });
|
|
244
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(3);
|
|
245
|
+
})
|
|
246
|
+
.then(() =>
|
|
247
|
+
Promise.all([
|
|
248
|
+
limiter.revert(),
|
|
249
|
+
limiter.revert(),
|
|
250
|
+
limiter.revert(),
|
|
251
|
+
limiter.revert(),
|
|
252
|
+
limiter.revert(),
|
|
253
|
+
limiter.revert(),
|
|
254
|
+
limiter.revert(),
|
|
255
|
+
limiter.revert(),
|
|
256
|
+
limiter.revert(),
|
|
257
|
+
limiter.revert(),
|
|
258
|
+
limiter.revert(),
|
|
259
|
+
limiter.revert(),
|
|
260
|
+
limiter.revert(),
|
|
261
|
+
limiter.revert(),
|
|
262
|
+
limiter.revert(),
|
|
263
|
+
limiter.revert(),
|
|
264
|
+
])
|
|
265
|
+
)
|
|
266
|
+
.then(() => {
|
|
267
|
+
expect(limiter._limitdRedis._num_tokens()).toBe(0);
|
|
268
|
+
done();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var Limiter = require("../limiter");
|
|
2
|
+
|
|
3
|
+
async function performTestWithTestPerMinute() {
|
|
4
|
+
console.log("Testing limitd-redius with bucket testPerMinute");
|
|
5
|
+
const limiter = new Limiter({
|
|
6
|
+
limitdUrl: "localhost:6379",
|
|
7
|
+
console: console,
|
|
8
|
+
});
|
|
9
|
+
// take 2 # Conformant - Remaining: 1
|
|
10
|
+
const result1 = await limiter.transfer("testPerMinute", "key", 2);
|
|
11
|
+
console.log("First Turn Take Result", result1);
|
|
12
|
+
|
|
13
|
+
//Now Conformant - Remaining: 1
|
|
14
|
+
const result2 = await limiter
|
|
15
|
+
.transfer("testPerMinute", "key", 2)
|
|
16
|
+
.catch((err) => {
|
|
17
|
+
console.log("Second Turn Take Error", err.message);
|
|
18
|
+
});
|
|
19
|
+
console.log("Second Turn Take Result", result2);
|
|
20
|
+
|
|
21
|
+
// Put 1
|
|
22
|
+
const result3 = await limiter.transfer("testPerMinute", "key", -1);
|
|
23
|
+
console.log("Third Turn PUT Result", result3);
|
|
24
|
+
|
|
25
|
+
//Now Conformant - Remaining: 2
|
|
26
|
+
const result4 = await limiter.transfer("testPerMinute", "key", 2);
|
|
27
|
+
console.log("Fourth Turn Take Result", result4);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
performTestWithTestPerMinute();
|
package/tests/sanityCheck.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
var Limiter = require("../limiter");
|
|
5
4
|
|
|
5
|
+
const buckets = {
|
|
6
|
+
emails: { size: 10 },
|
|
7
|
+
emailRate: { size: 10, per_hour: 10 },
|
|
8
|
+
};
|
|
9
|
+
|
|
6
10
|
var limiter = new Limiter({
|
|
7
|
-
limitdUrl: "
|
|
8
|
-
console: console
|
|
9
|
-
|
|
11
|
+
limitdUrl: "localhost:6379",
|
|
12
|
+
console: console,
|
|
13
|
+
buckets,
|
|
14
|
+
});
|
|
10
15
|
|
|
11
16
|
var callback = function (err) {
|
|
12
17
|
if (err) {
|
|
@@ -18,10 +23,10 @@ var callback = function (err) {
|
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
var withdrawls = [
|
|
26
|
+
["emails", process.argv[2], -parseInt(process.argv[3])],
|
|
27
|
+
["emailRate", process.argv[2], -parseInt(process.argv[3])],
|
|
21
28
|
["emails", process.argv[2], parseInt(process.argv[3])],
|
|
22
|
-
["emailRate", process.argv[2], parseInt(process.argv[3])]
|
|
29
|
+
["emailRate", process.argv[2], parseInt(process.argv[3])],
|
|
23
30
|
];
|
|
24
31
|
|
|
25
|
-
var withdrawlsComplete = [];
|
|
26
32
|
limiter.transferAll(withdrawls).then(() => callback(), callback);
|
|
27
|
-
|