Add cache, test

This commit is contained in:
Calvin Montgomery 2022-09-19 22:59:06 -07:00
parent d61af3f9d5
commit 5985c8d280
9 changed files with 230 additions and 47 deletions

View file

@ -362,6 +362,7 @@ describe('KickbanModule', () => {
); );
}); });
// TODO: for whatever reason, this test is flaky
it('inserts a valid IPv6 ban', done => { it('inserts a valid IPv6 ban', done => {
const longIP = require('../../lib/utilities').expandIPv6('::abcd'); const longIP = require('../../lib/utilities').expandIPv6('::abcd');

View file

@ -0,0 +1,109 @@
const assert = require('assert');
const { BannedChannelsController } = require('../../lib/controller/banned-channels');
const dbChannels = require('../../lib/database/channels');
const testDB = require('../testutil/db').testDB;
const { EventEmitter } = require('events');
require('../../lib/database').init(testDB);
const testBan = {
name: 'ban_test_1',
externalReason: 'because I said so',
internalReason: 'illegal content',
bannedBy: 'admin'
};
async function cleanupTestBan() {
return dbChannels.removeBannedChannel(testBan.name);
}
describe('BannedChannelsController', () => {
let controller;
let messages;
beforeEach(async () => {
await cleanupTestBan();
messages = new EventEmitter();
controller = new BannedChannelsController(
dbChannels,
messages
);
});
afterEach(async () => {
await cleanupTestBan();
});
it('bans a channel', async () => {
assert.strictEqual(await controller.getBannedChannel(testBan.name), null);
let received = null;
messages.once('ChannelBanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
let info = await controller.getBannedChannel(testBan.name);
for (let field of Object.keys(testBan)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan.name);
} else {
assert.strictEqual(info[field], testBan[field]);
}
}
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
assert.strictEqual(received.externalReason, testBan.externalReason);
});
it('updates an existing ban', async () => {
let received = [];
messages.on('ChannelBanned', cb => {
received.push(cb);
});
await controller.banChannel(testBan);
let testBan2 = { ...testBan, externalReason: 'because of reasons' };
await controller.banChannel(testBan2);
let info = await controller.getBannedChannel(testBan2.name);
for (let field of Object.keys(testBan2)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan2.name);
} else {
assert.strictEqual(info[field], testBan2[field]);
}
}
assert.deepStrictEqual(received, [
{
channel: testBan.name,
externalReason: testBan.externalReason
},
{
channel: testBan2.name,
externalReason: testBan2.externalReason
},
]);
});
it('unbans a channel', async () => {
let received = null;
messages.once('ChannelUnbanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
await controller.unbanChannel(testBan.name, testBan.bannedBy);
let info = await controller.getBannedChannel(testBan.name);
assert.strictEqual(info, null);
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
});
});

View file

@ -1,10 +1,15 @@
import { eventlog } from '../logger'; import { eventlog } from '../logger';
import { SimpleCache } from '../util/simple-cache';
const LOGGER = require('@calzoneman/jsli')('BannedChannelsController'); const LOGGER = require('@calzoneman/jsli')('BannedChannelsController');
export class BannedChannelsController { export class BannedChannelsController {
constructor(dbChannels, globalMessageBus) { constructor(dbChannels, globalMessageBus) {
this.dbChannels = dbChannels; this.dbChannels = dbChannels;
this.globalMessageBus = globalMessageBus; this.globalMessageBus = globalMessageBus;
this.cache = new SimpleCache({
maxElem: 1000,
maxAge: 5 * 60_000
});
} }
/* /*
@ -20,6 +25,8 @@ export class BannedChannelsController {
LOGGER.warn(`Channel ${name} is already banned, updating ban reason`); LOGGER.warn(`Channel ${name} is already banned, updating ban reason`);
} }
this.cache.delete(name);
await this.dbChannels.putBannedChannel({ await this.dbChannels.putBannedChannel({
name, name,
externalReason, externalReason,
@ -36,6 +43,7 @@ export class BannedChannelsController {
async unbanChannel(name, unbannedBy) { async unbanChannel(name, unbannedBy) {
LOGGER.info(`Unbanning channel ${name}`); LOGGER.info(`Unbanning channel ${name}`);
eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`); eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`);
this.cache.delete(name);
this.globalMessageBus.emit( this.globalMessageBus.emit(
'ChannelUnbanned', 'ChannelUnbanned',
@ -46,47 +54,14 @@ export class BannedChannelsController {
} }
async getBannedChannel(name) { async getBannedChannel(name) {
// TODO: cache name = name.toLowerCase();
return this.dbChannels.getBannedChannel(name);
} let info = this.cache.get(name);
} if (info === null) {
info = await this.dbChannels.getBannedChannel(name);
class Cache { this.cache.put(name, info);
constructor({ maxElem, maxAge }) { }
this.maxElem = maxElem;
this.maxAge = maxAge; return info;
this.cache = new Map();
}
put(key, value) {
this.cache.set(key, { value: value, at: Date.now() });
if (this.cache.size > this.maxElem) {
this.cache.delete(this.cache.keys().next());
}
}
get(key) {
let val = this.cache.get(key);
if (val != null) {
return val.value;
} else {
return null;
}
}
delete(key) {
this.cache.delete(key);
}
cleanup() {
let now = Date.now();
for (let [key, value] of this.cache) {
if (value.at < now - this.maxAge) {
this.cache.delete(key);
}
}
} }
} }

View file

@ -59,6 +59,7 @@ function handleLine(line, client) {
client.write('{"status":"error","error":"internal error"}\n'); client.write('{"status":"error","error":"internal error"}\n');
}); });
} catch (_error) { } catch (_error) {
// eslint no-empty: off
} }
if (line === '/reload') { if (line === '/reload') {

View file

@ -141,7 +141,8 @@ var Server = function () {
Config.getEmailConfig(), Config.getEmailConfig(),
emailController, emailController,
Config.getCaptchaConfig(), Config.getCaptchaConfig(),
captchaController captchaController,
self.bannedChannelsController
); );
// http/https/sio server init ----------------------------------------- // http/https/sio server init -----------------------------------------

45
src/util/simple-cache.js Normal file
View file

@ -0,0 +1,45 @@
class SimpleCache {
constructor({ maxElem, maxAge }) {
this.maxElem = maxElem;
this.maxAge = maxAge;
this.cache = new Map();
setInterval(() => {
this.cleanup();
}, maxAge).unref();
}
put(key, value) {
this.cache.set(key, { value: value, at: Date.now() });
if (this.cache.size > this.maxElem) {
this.cache.delete(this.cache.keys().next().value);
}
}
get(key) {
let val = this.cache.get(key);
if (val != null && Date.now() < val.at + this.maxAge) {
return val.value;
} else {
return null;
}
}
delete(key) {
this.cache.delete(key);
}
cleanup() {
let now = Date.now();
for (let [key, value] of this.cache) {
if (value.at < now - this.maxAge) {
this.cache.delete(key);
}
}
}
}
export { SimpleCache };

View file

@ -11,7 +11,6 @@ export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
'channel name.', { status: HTTPStatus.NOT_FOUND }); 'channel name.', { status: HTTPStatus.NOT_FOUND });
} }
// TODO: add a cache
let banInfo = await getBannedChannel(req.params.channel); let banInfo = await getBannedChannel(req.params.channel);
if (banInfo !== null) { if (banInfo !== null) {
sendPug(res, 'banned_channel', { sendPug(res, 'banned_channel', {

View file

@ -143,7 +143,8 @@ module.exports = {
emailConfig, emailConfig,
emailController, emailController,
captchaConfig, captchaConfig,
captchaController captchaController,
bannedChannelsController
) { ) {
patchExpressToHandleAsync(); patchExpressToHandleAsync();
const chanPath = Config.get('channel-path'); const chanPath = Config.get('channel-path');
@ -198,8 +199,7 @@ module.exports = {
app, app,
ioConfig, ioConfig,
chanPath, chanPath,
// TODO: banController async name => bannedChannelsController.getBannedChannel(name)
require('../database/channels').getBannedChannel
); );
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries()); require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
require('./routes/socketconfig')(app, clusterClient); require('./routes/socketconfig')(app, clusterClient);

52
test/util/simple-cache.js Normal file
View file

@ -0,0 +1,52 @@
const { SimpleCache } = require('../../lib/util/simple-cache');
const assert = require('assert');
describe('SimpleCache', () => {
const CACHE_MAX_ELEM = 5;
const CACHE_MAX_AGE = 5;
let cache;
beforeEach(() => {
cache = new SimpleCache({
maxElem: CACHE_MAX_ELEM,
maxAge: CACHE_MAX_AGE
});
});
it('sets, gets, and deletes a value', () => {
assert.strictEqual(cache.get('foo'), null);
cache.put('foo', 'bar');
assert.strictEqual(cache.get('foo'), 'bar');
cache.delete('foo');
assert.strictEqual(cache.get('foo'), null);
});
it('does not return an expired value', done => {
cache.put('foo', 'bar');
setTimeout(() => {
assert.strictEqual(cache.get('foo'), null);
done();
}, CACHE_MAX_AGE + 1);
});
it('cleans up old values', done => {
cache.put('foo', 'bar');
setTimeout(() => {
assert.strictEqual(cache.get('foo'), null);
done();
}, CACHE_MAX_AGE * 2);
});
it('removes the oldest entry if max elem is reached', () => {
for (let i = 0; i < CACHE_MAX_ELEM + 1; i++) {
cache.put(`foo${i}`, 'bar');
}
assert.strictEqual(cache.get('foo0'), null);
assert.strictEqual(cache.get('foo1'), 'bar');
});
});