Skip to content

Commit e745138

Browse files
SGrondingr2m
authored andcommitted
feat: Clustering support using Redis (#20)
1 parent 8cf0d13 commit e745138

File tree

3 files changed

+80
-9
lines changed

3 files changed

+80
-9
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,57 @@ async function createIssueOnAllRepos (org) {
5555

5656
Pass `{ throttle: { enabled: false } }` to disable this plugin.
5757

58+
### Clustering
59+
60+
Enabling Clustering support ensures that your application will not go over rate limits **across Octokit instances and across Nodejs processes**.
61+
62+
First install either `redis` or `ioredis`:
63+
```
64+
# NodeRedis (https://github.com/NodeRedis/node_redis)
65+
npm install --save redis
66+
67+
# or ioredis (https://github.com/luin/ioredis)
68+
npm install --save ioredis
69+
```
70+
71+
Then in your application:
72+
```js
73+
const Bottleneck = require('bottleneck')
74+
const Redis = require('redis')
75+
76+
const client = Redis.createClient({ /* options */ })
77+
const connection = new Bottleneck.RedisConnection({ client })
78+
connection.on('error', err => console.error(err))
79+
80+
const octokit = new Octokit({
81+
throttle: {
82+
onAbuseLimit: (retryAfter, options) => { /* ... */ },
83+
onRateLimit: (retryAfter, options) => { /* ... */ },
84+
85+
// The Bottleneck connection object
86+
connection,
87+
88+
// A "throttling ID". All octokit instances with the same ID
89+
// using the same Redis server will share the throttling.
90+
id: 'my-super-app',
91+
92+
// Otherwise the plugin uses a lighter version of Bottleneck without Redis support
93+
Bottleneck
94+
}
95+
})
96+
97+
// To close the connection and allow your application to exit cleanly:
98+
await connection.disconnect()
99+
```
100+
101+
To use the `ioredis` library instead:
102+
```js
103+
const Redis = require('ioredis')
104+
const client = new Redis({ /* options */ })
105+
const connection = new Bottleneck.IORedisConnection({ client })
106+
connection.on('error', err => console.error(err))
107+
```
108+
58109
## LICENSE
59110

60111
[MIT](LICENSE)

lib/index.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = throttlingPlugin
22

3-
const Bottleneck = require('bottleneck/light')
3+
const BottleneckLight = require('bottleneck/light')
44
const wrapRequest = require('./wrap-request')
55
const triggersNotificationPaths = require('./triggers-notification-paths')
66
const routeMatcher = require('./route-matcher')(triggersNotificationPaths)
@@ -10,29 +10,44 @@ const triggersNotification = throttlingPlugin.triggersNotification =
1010
routeMatcher.test.bind(routeMatcher)
1111

1212
function throttlingPlugin (octokit, octokitOptions = {}) {
13+
const {
14+
enabled = true,
15+
Bottleneck = BottleneckLight,
16+
id = 'no-id',
17+
connection
18+
} = octokitOptions.throttle || {}
19+
if (!enabled) {
20+
return
21+
}
22+
const common = {
23+
connection,
24+
timeout: 1000 * 60 * 10 // Redis TTL: 10 minutes
25+
}
1326
const state = Object.assign({
14-
enabled: true,
27+
clustering: connection != null,
1528
triggersNotification,
1629
minimumAbuseRetryAfter: 5,
1730
retryAfterBaseValue: 1000,
1831
globalLimiter: new Bottleneck({
19-
maxConcurrent: 1
32+
id: `octokit-global-${id}`,
33+
maxConcurrent: 1,
34+
...common
2035
}),
2136
writeLimiter: new Bottleneck({
37+
id: `octokit-write-${id}`,
2238
maxConcurrent: 1,
23-
minTime: 1000
39+
minTime: 1000,
40+
...common
2441
}),
2542
triggersNotificationLimiter: new Bottleneck({
43+
id: `octokit-notifications-${id}`,
2644
maxConcurrent: 1,
27-
minTime: 3000
45+
minTime: 3000,
46+
...common
2847
}),
2948
retryLimiter: new Bottleneck()
3049
}, octokitOptions.throttle)
3150

32-
if (!state.enabled) {
33-
return
34-
}
35-
3651
if (typeof state.onAbuseLimit !== 'function' || typeof state.onRateLimit !== 'function') {
3752
throw new Error(`octokit/plugin-throttling error:
3853
You must pass the onAbuseLimit and onRateLimit error handlers.

lib/wrap-request.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ async function doRequest (state, request, options) {
1010
const isWrite = options.method !== 'GET' && options.method !== 'HEAD'
1111
const retryCount = ~~options.request.retryCount
1212
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {}
13+
if (state.clustering) {
14+
// Remove a job from Redis if it has not completed or failed within 60s
15+
// Examples: Node process terminated, client disconnected, etc.
16+
jobOptions.expiration = 1000 * 60
17+
}
1318

1419
// Guarantee at least 1000ms between writes
1520
if (isWrite) {

0 commit comments

Comments
 (0)