Skip to content

Commit

Permalink
feat: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Grondin authored and gr2m committed Dec 27, 2018
1 parent a102985 commit 034a34d
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 0 deletions.
55 changes: 55 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = throttlingPlugin

const Bottleneck = require('bottleneck/light')
const wrapRequest = require('./wrap-request')

const triggersNotificationRoutes = [
'/repos/:owner/:repo/issues',
'/repos/:owner/:repo/issues/:number/comments',
'/orgs/:org/invitations',
'/repos/:owner/:repo/pulls',
'/repos/:owner/:repo/pulls',
'/repos/:owner/:repo/pulls/:number/merge',
'/repos/:owner/:repo/pulls/:number/reviews',
'/repos/:owner/:repo/pulls/:number/comments',
'/repos/:owner/:repo/pulls/:number/comments',
'/repos/:owner/:repo/pulls/:number/requested_reviewers',
'/repos/:owner/:repo/collaborators/:username',
'/repos/:owner/:repo/commits/:sha/comments',
'/repos/:owner/:repo/releases',
'/teams/:team_id/discussions',
'/teams/:team_id/discussions/:discussion_number/comments'
]

function buildLookup (arr) {
return arr.reduce(function (acc, elem) {
acc[elem] = true
return acc
}, {})
}

function throttlingPlugin (octokit) {
const state = {
triggersNotification: buildLookup(triggersNotificationRoutes),
minimumAbuseRetryAfter: 5,
maxRetries: 1,
globalLimiter: new Bottleneck({
maxConcurrent: 1
}),
writeLimiter: new Bottleneck({
maxConcurrent: 1,
minTime: 1000
}),
triggersNotificationLimiter: new Bottleneck({
maxConcurrent: 1,
minTime: 3000
})
}

octokit.throttle = {
options: (options = {}) => Object.assign(state, options)
}
const emitter = new Bottleneck.Events(octokit.throttle)

octokit.hook.wrap('request', wrapRequest.bind(null, state, emitter))
}
61 changes: 61 additions & 0 deletions lib/wrap-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint padded-blocks: 0 */
module.exports = wrapRequest

const noop = () => Promise.resolve()

async function wrapRequest (state, emitter, request, options) {
const retryRequest = function (after) {
return new Promise(resolve => setTimeout(resolve, after * 1000))
.then(() => wrapRequest(state, emitter, request, options))
}
const isWrite = options.method !== 'GET' && options.method !== 'HEAD'
const retryCount = ~~options.request.retryCount
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {}

// Guarantee at least 1000ms between writes
if (isWrite) {
await state.writeLimiter.schedule(noop)
}

// Guarantee at least 3000ms between requests that trigger notifications
if (isWrite && state.triggersNotification[options.url]) {
await state.triggersNotificationLimiter.schedule(noop)
}

return state.globalLimiter.schedule(jobOptions, async function () {
try {
// Execute request
return await request(options)
} catch (error) {
if (error.status === 403 && /\babuse\b/i.test(error.message)) {
// The user has hit the abuse rate limit.
// https://developer.github.com/v3/#abuse-rate-limits

// The Retry-After header can sometimes be blank when hitting an abuse limit,
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
const retryAfter = Math.max(~~error.headers['retry-after'], state.minimumAbuseRetryAfter)
emitter.trigger('abuse-limit', retryAfter)

if (state.maxRetries > retryCount) {
options.request.retryCount = retryCount + 1
return retryRequest(retryAfter)
}

} else if (error.status === 403 && error.headers['x-ratelimit-remaining'] === '0') {
// The user has used all their allowed calls for the current time period
// https://developer.github.com/v3/#rate-limiting

const rateLimitReset = new Date(~~error.headers['x-ratelimit-reset'] * 1000).getTime()
const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0)
emitter.trigger('rate-limit', retryAfter)

if (state.maxRetries > retryCount) {
options.request.retryCount = retryCount + 1
return retryRequest(retryAfter)
}
}

throw error
}
})
}

0 comments on commit 034a34d

Please sign in to comment.