Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions lib/api/apiUtils/rateLimit/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const counters = new Map();

const configCache = new Map();

function setCounter(key, value) {
// Make sure that the Map remains in order
// Counters expiring soonest will be first during iteration.
counters.delete(key);
counters.set(key, value);
}

function getCounter(key) {
return counters.get(key);
}

function expireCounters(now) {
const toRemove = [];
for (const [key, value] of counters.entries()) {
if (value <= now) {
toRemove.push(key);
}
}

for (const key of toRemove) {
counters.delete(key);
}
}

function setCachedConfig(key, limitConfig, ttl) {
const expiry = Date.now() + ttl;
configCache.set(key, { expiry, config: limitConfig });
}

function getCachedConfig(key) {
const value = configCache.get(key);
if (value === undefined) {
return undefined;
}

const { expiry, config } = value;
if (expiry <= Date.now()) {
configCache.delete(key);
return undefined;
}

return config;
}

function expireCachedConfigs(now) {
const toRemove = [];
for (const [key, { expiry }] of configCache.entries()) {
if (expiry <= now) {
toRemove.push(key);
}
}

for (const key of toRemove) {
configCache.delete(key);
}
}

module.exports = {
setCounter,
getCounter,
expireCounters,
setCachedConfig,
getCachedConfig,
expireCachedConfigs,

// Do not access directly
// Used only for tests
counters,
configCache,
};
110 changes: 110 additions & 0 deletions tests/unit/api/apiUtils/rateLimit/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const assert = require('assert');
const sinon = require('sinon');

const constants = require('../../../../../constants');
const {
counters,
configCache,
getCounter,
setCounter,
expireCounters,
getCachedConfig,
setCachedConfig,
expireCachedConfigs,
} = require('../../../../../lib/api/apiUtils/rateLimit/cache');

describe('test counter storage', () => {
it('should set a counter', () => {
setCounter('foo', 10);
assert.strictEqual(counters.get('foo'), 10);
});

it('should get a counter', () => {
setCounter('foo', 10);
assert.strictEqual(getCounter('foo'), 10);
});

it('should maintain order when updating a counter', () => {
setCounter('foo', 10);
setCounter('bar', 20);
setCounter('foo', 30);

const items = Array.from(counters.entries());
assert.deepStrictEqual(items, [
['bar', 20],
['foo', 30],
]);
});

it('should expire counters less than or equal to the given timestamp', () => {
const now = Date.now();
const past = now - 100;
const future = now + 100;
setCounter('past', past);
setCounter('present', now);
setCounter('future', future);
expireCounters(now);
assert.strictEqual(getCounter('past'), undefined);
assert.strictEqual(getCounter('present'), undefined);
assert.strictEqual(getCounter('future'), future);
});
});

describe('test limit config cache storage', () => {
const now = Date.now();

let clock;
before(() => {
clock = sinon.useFakeTimers(now);
});

after(() => {
clock.restore();
});

it('should add config to cache', () => {
setCachedConfig('foo', 10, constants.rateLimitDefaultConfigCacheTTL);
assert.deepStrictEqual(
configCache.get('foo'),
{
expiry: now + constants.rateLimitDefaultConfigCacheTTL,
config: 10,
}
);
});

it('should get a non expired config', () => {
setCachedConfig('foo', 10, constants.rateLimitDefaultConfigCacheTTL);
assert.strictEqual(getCachedConfig('foo'), 10);
});

it('should return undefined and delete the key for an expired config', () => {
configCache.set('foo', {
expiry: now - 10000,
config: 10,
});
assert.strictEqual(getCachedConfig('foo'), undefined);
});

it('should expire configs less than or equal to the given timestamp', () => {
configCache.set('past', {
expiry: now - 10000,
config: 10,
});
configCache.set('present', {
expiry: now,
config: 10,
});
configCache.set('future', {
expiry: now + 10000,
config: 10,
});
expireCachedConfigs(now);
assert.strictEqual(configCache.get('past'), undefined);
assert.strictEqual(configCache.get('present'), undefined);
assert.deepStrictEqual(configCache.get('future'), {
expiry: now + 10000,
config: 10,
});
});
});
Loading