Feedback from review

This commit is contained in:
gnuxie 2022-02-14 16:59:35 +00:00
parent f229716150
commit 580a8f6b10
3 changed files with 32 additions and 23 deletions

View File

@ -26,7 +26,7 @@ import {
UserID UserID
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import BanList, { ALL_RULE_TYPES, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/BanList"; import BanList, { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/BanList";
import { applyServerAcls } from "./actions/ApplyAcl"; import { applyServerAcls } from "./actions/ApplyAcl";
import { RoomUpdateError } from "./models/RoomUpdateError"; import { RoomUpdateError } from "./models/RoomUpdateError";
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
@ -764,7 +764,7 @@ export class Mjolnir {
/** /**
* Pulls any changes to the rules that are in a policy room and updates all protected rooms * Pulls any changes to the rules that are in a policy room and updates all protected rooms
* with those changes. Does not fail if there are errors updating the room, these are reported to the management room. * with those changes. Does not fail if there are errors updating the room, these are reported to the management room.
* @param policyRoomId The room with a policy list which we will check for changes and apply them to all protected rooms. * @param banList The `BanList` which we will check for changes and apply them to all protected rooms.
* @returns When all of the protected rooms have been updated. * @returns When all of the protected rooms have been updated.
*/ */
public async syncWithBanList(banList: BanList): Promise<void> { public async syncWithBanList(banList: BanList): Promise<void> {
@ -809,7 +809,7 @@ export class Mjolnir {
// themselves. // themselves.
const banList = this.banLists.find(list => list.roomId === roomId); const banList = this.banLists.find(list => list.roomId === roomId);
if (banList !== undefined) { if (banList !== undefined) {
if (ALL_RULE_TYPES.includes(event['type'])) { if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') {
banList.updateForEvent(event) banList.updateForEvent(event)
} }
} }

View File

@ -72,8 +72,10 @@ export interface ListRuleChange {
} }
declare interface BanList { declare interface BanList {
// BanList.update is emitted when the BanList has pulled new rules from Matrix and informs listeners of any changes.
on(event: 'BanList.update', listener: (list: BanList, changes: ListRuleChange[]) => void): this on(event: 'BanList.update', listener: (list: BanList, changes: ListRuleChange[]) => void): this
emit(event: 'BanList.update', list: BanList, changes: ListRuleChange[]): boolean emit(event: 'BanList.update', list: BanList, changes: ListRuleChange[]): boolean
// BanList.batch is emitted when the BanList has created a batch from the events provided by `updateForEvent`.
on(event: 'BanList.batch', listener: (list: BanList) => void): this on(event: 'BanList.batch', listener: (list: BanList) => void): this
emit(event: 'BanList.batch', list: BanList): boolean emit(event: 'BanList.batch', list: BanList): boolean
} }
@ -292,11 +294,6 @@ class BanList extends EventEmitter {
* @param event An event from the room the `BanList` models to inform an instance about. * @param event An event from the room the `BanList` models to inform an instance about.
*/ */
public updateForEvent(event: { event_id: string }): void { public updateForEvent(event: { event_id: string }): void {
// We have to allow the batcher to emit BanList.batch because
// if we await in the updateForEvent method that is called by Mjolnir's sync
// event emitter, then by the time we start batching we will be far too late
// and unable to batch effectivly
// if you don't believe me you can test it for yourself, it is rubbish.
this.batcher.addToBatch(event.event_id) this.batcher.addToBatch(event.event_id)
} }
} }
@ -308,10 +305,12 @@ export default BanList;
* out of the events given to `addToBatch`. * out of the events given to `addToBatch`.
*/ */
class UpdateBatcher { class UpdateBatcher {
// Whether we are waiting for more events to form a batch.
private isWaiting = false; private isWaiting = false;
private previousEventId: string|null = null; // The latest (or most recent) event we have received.
private readonly waitPeriod = 200; // 200ms seems good enough. private latestEventId: string|null = null;
private readonly maxWait = 3000; // 3s is long enough to wait while batching. private readonly waitPeriodMS = 200; // 200ms seems good enough.
private readonly maxWaitMS = 3000; // 3s is long enough to wait while batching.
constructor(private readonly banList: BanList) { constructor(private readonly banList: BanList) {
@ -321,7 +320,7 @@ class UpdateBatcher {
* Reset the state for the next batch. * Reset the state for the next batch.
*/ */
private reset() { private reset() {
this.previousEventId = null; this.latestEventId = null;
this.isWaiting = false; this.isWaiting = false;
} }
@ -334,8 +333,8 @@ class UpdateBatcher {
private async checkBatch(eventId: string): Promise<void> { private async checkBatch(eventId: string): Promise<void> {
let start = Date.now(); let start = Date.now();
do { do {
await new Promise(resolve => setTimeout(resolve, this.waitPeriod)); await new Promise(resolve => setTimeout(resolve, this.waitPeriodMS));
} while ((Date.now() - start) < this.maxWait && this.previousEventId !== eventId) } while ((Date.now() - start) < this.maxWaitMS && this.latestEventId !== eventId)
this.reset(); this.reset();
this.banList.emit('BanList.batch', this.banList); this.banList.emit('BanList.batch', this.banList);
} }
@ -346,11 +345,18 @@ class UpdateBatcher {
*/ */
public addToBatch(eventId: string): void { public addToBatch(eventId: string): void {
if (this.isWaiting) { if (this.isWaiting) {
this.previousEventId = eventId; this.latestEventId = eventId;
return; return;
} }
this.previousEventId = eventId; this.latestEventId = eventId;
this.isWaiting = true; this.isWaiting = true;
// We 'spawn' off here after performing the checks above
// rather than before (ie if `addToBatch` was async) because
// `banListTest` showed that there were 100~ ACL events per protected room
// as compared to just 5~ by doing this. Not entirely sure why but it probably
// has to do with queuing up `n event` tasks on the event loop that exaust scheduling
// (so the latency between them is percieved as much higher by
// the time they get checked in `this.checkBatch`, thus batching fails).
this.checkBatch(eventId); this.checkBatch(eventId);
} }
} }

View File

@ -237,7 +237,8 @@ describe.only('Test: ACL updates will batch when rules are added in succession.'
const moderator = await newTestUser({ name: { contains: "moderator" }}); const moderator = await newTestUser({ name: { contains: "moderator" }});
moderator.joinRoom(this.mjolnir.managementRoomId); moderator.joinRoom(this.mjolnir.managementRoomId);
const mjolnirId = await mjolnir.getUserId(); const mjolnirId = await mjolnir.getUserId();
// How many rooms are we going to protect? Do we need to protect a few and verify all the ACL updates were batched in each?
// Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = []; const protectedRooms: string[] = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId]}); const room = await moderator.createRoom({ invite: [mjolnirId]});
@ -255,22 +256,24 @@ describe.only('Test: ACL updates will batch when rules are added in succession.'
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
})); }));
// Flood the subsribed list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms. // Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
const banListId = await moderator.createRoom({ invite: [mjolnirId] }); const banListId = await moderator.createRoom({ invite: [mjolnirId] });
mjolnir.joinRoom(banListId); mjolnir.joinRoom(banListId);
this.mjolnir!.watchList(Permalinks.forRoom(banListId)); this.mjolnir!.watchList(Permalinks.forRoom(banListId));
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*"); const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
for (let i = 0; i < 200; i++) { for (let i = 0; i < 200; i++) {
acl.denyServer(i.toString()); const badServer = `${i}.evil.com`;
await createPolicyRule(moderator, banListId, RULE_SERVER, `${i}`, `Rule #${i}`); acl.denyServer(badServer);
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`);
// Give them a bit of a spread over time. // Give them a bit of a spread over time.
await new Promise(resolve => setTimeout(resolve, 5)); await new Promise(resolve => setTimeout(resolve, 5));
} }
// We do this because it should force us to wait until all the ACL events have been applied. // We do this because it should force us to wait until all the ACL events have been applied.
// Even if that does mean the last few events will not go through batching...
await this.mjolnir!.syncLists(); await this.mjolnir!.syncLists();
// Check each of the protected rooms. // Check each of the protected rooms for ACL events and make sure they were batched and are correct.
await Promise.all(protectedRooms.map(async room => { await Promise.all(protectedRooms.map(async room => {
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", ""); const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");
if (!acl.matches(roomAcl)) { if (!acl.matches(roomAcl)) {
@ -281,8 +284,8 @@ describe.only('Test: ACL updates will batch when rules are added in succession.'
events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null); events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
}); });
LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`); LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`);
// If there's less than two then it probably means the ACL was put in by this test calling syncLists // If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()`
// and not the listener that detects changes to ban lists. // and not the listener that detects changes to ban lists (that we want to test!).
assert.equal(aclEventCount < 10 && aclEventCount > 2, true, 'We should have sent less than 10 ACL events to each room because they should be batched') assert.equal(aclEventCount < 10 && aclEventCount > 2, true, 'We should have sent less than 10 ACL events to each room because they should be batched')
})); }));
}) })