Skip to content

Commit

Permalink
chore: RateLimiter to async (#31480)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo authored Jan 17, 2024
1 parent 25403da commit 6f72f4c
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 8 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {
}

rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.increment(objectForRateLimitMatch);
const attemptResult = rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch);
const attemptResult = await rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch);
const timeToResetAttempsInSeconds = Math.ceil(attemptResult.timeToReset / 1000);
response.setHeader('X-RateLimit-Limit', rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed);
response.setHeader('X-RateLimit-Remaining', attemptResult.numInvocationsLeft);
Expand Down
55 changes: 51 additions & 4 deletions apps/meteor/app/lib/server/lib/RateLimiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,68 @@ export const RateLimiterClass = new (class {
if (process.env.TEST_MODE === 'true') {
return fn;
}
const rateLimiter = new RateLimiter();
const rateLimiter = new (class extends RateLimiter {
async check(input) {
const reply = {
allowed: true,
timeToReset: 0,
numInvocationsLeft: Infinity,
};

const matchedRules = this._findAllMatchingRules(input);

for await (const rule of matchedRules) {
const ruleResult = await rule.apply(input);
let numInvocations = rule.counters[ruleResult.key];

if (ruleResult.timeToNextReset < 0) {
// Reset all the counters since the rule has reset
await rule.resetCounter();
ruleResult.timeSinceLastReset = new Date().getTime() - rule._lastResetTime;
ruleResult.timeToNextReset = rule.options.intervalTime;
numInvocations = 0;
}

if (numInvocations > rule.options.numRequestsAllowed) {
// Only update timeToReset if the new time would be longer than the
// previously set time. This is to ensure that if this input triggers
// multiple rules, we return the longest period of time until they can
// successfully make another call
if (reply.timeToReset < ruleResult.timeToNextReset) {
reply.timeToReset = ruleResult.timeToNextReset;
}
reply.allowed = false;
reply.numInvocationsLeft = 0;
reply.ruleId = rule.id;
await rule._executeCallback(reply, input);
} else {
// If this is an allowed attempt and we haven't failed on any of the
// other rules that match, update the reply field.
if (rule.options.numRequestsAllowed - numInvocations < reply.numInvocationsLeft && reply.allowed) {
reply.timeToReset = ruleResult.timeToNextReset;
reply.numInvocationsLeft = rule.options.numRequestsAllowed - numInvocations;
}
reply.ruleId = rule.id;
await rule._executeCallback(reply, input);
}
}
return reply;
}
})();
Object.entries(matchers).forEach(([key, matcher]) => {
matchers[key] = (...args) => Promise.await(matcher(...args));
matchers[key] = matcher;
});

rateLimiter.addRule(matchers, numRequests, timeInterval);
return function (...args) {
return async function (...args) {
const match = {};

Object.keys(matchers).forEach((key) => {
match[key] = args[key];
});

rateLimiter.increment(match);
const rateLimitResult = rateLimiter.check(match);
const rateLimitResult = await rateLimiter.check(match);
if (rateLimitResult.allowed) {
return fn.apply(null, args);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/lib/server/startup/rateLimiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const checkNameForStream = (name) => name && !names.has(name) && name.startsWith

const ruleIds = {};

const callback = (msg, name) => (reply, input) => {
const callback = (msg, name) => async (reply, input) => {
if (reply.allowed === false) {
rateLimiterLog({ msg, reply, input });
metrics.ddpRateLimitExceeded.inc({
Expand All @@ -136,7 +136,7 @@ const callback = (msg, name) => (reply, input) => {
});
// sleep before sending the error to slow down next requests
if (slowDownRate > 0 && reply.numInvocationsExceeded) {
Promise.await(sleep(slowDownRate * reply.numInvocationsExceeded));
await sleep(slowDownRate * reply.numInvocationsExceeded);
}
// } else {
// console.log('DDP RATE LIMIT:', message);
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/definition/externals/meteor/rate-limit.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ declare module 'meteor/rate-limit' {
route: string;
};

type RateLimiterCheckResult = {
allowed: boolean;
timeToReset: number;
numInvocationsLeft: number;
};

class RateLimiter {
public check(input: RateLimiterOptionsToCheck);
public check(input: RateLimiterOptionsToCheck): Promise<RateLimiterCheckResult>;

public increment(input: RateLimiterOptionsToCheck);

Expand Down

0 comments on commit 6f72f4c

Please sign in to comment.