From ca94dea20f85c561f0b4f4238c32e465f07f2de2 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sat, 9 Jan 2021 17:49:30 -0600 Subject: [PATCH] FS: Fix hotpatching and add more throttling (#7878) - `writeUpdate` state is now stored in a global variable, so hotpatching doesn't crash it - throttling now writes on the tail (so two throttled `writeUpdate` calls will write one update, not two) - room settings, punishments, and helptickets are now throttled --- lib/fs.ts | 35 +++++++++++++++++++++--------- server/chat-plugins/helptickets.ts | 2 +- server/punishments.ts | 4 ++-- server/rooms.ts | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/lib/fs.ts b/lib/fs.ts index 73c8e366b8..86fb5a0e9a 100644 --- a/lib/fs.ts +++ b/lib/fs.ts @@ -35,7 +35,13 @@ interface PendingUpdate { throttleTimer: NodeJS.Timer | null; } -const pendingUpdates = new Map(); +declare const __fsState: {pendingUpdates: Map}; +declare const global: {__fsState: typeof __fsState}; +if (!global.__fsState) { + global.__fsState = { + pendingUpdates: new Map(), + }; +} export class FSPath { path: string; @@ -152,9 +158,8 @@ export class FSPath { */ writeUpdate(dataFetcher: () => string | Buffer, options: AnyObject = {}) { if (Config.nofswriting) return; - const pendingUpdate: PendingUpdate | undefined = pendingUpdates.get(this.path); + const pendingUpdate: PendingUpdate | undefined = __fsState.pendingUpdates.get(this.path); - // @ts-ignore const throttleTime = options.throttle ? Date.now() + options.throttle : 0; if (pendingUpdate) { @@ -168,11 +173,22 @@ export class FSPath { return; } - this.writeUpdateNow(dataFetcher, options); + if (!throttleTime) { + this.writeUpdateNow(dataFetcher, options); + return; + } + + const update: PendingUpdate = { + isWriting: false, + pendingDataFetcher: dataFetcher, + pendingOptions: options, + throttleTime, + throttleTimer: setTimeout(() => this.checkNextUpdate(), throttleTime - Date.now()), + }; + __fsState.pendingUpdates.set(this.path, update); } writeUpdateNow(dataFetcher: () => string | Buffer, options: AnyObject) { - // @ts-ignore const throttleTime = options.throttle ? Date.now() + options.throttle : 0; const update = { isWriting: true, @@ -181,25 +197,25 @@ export class FSPath { throttleTime, throttleTimer: null, }; - pendingUpdates.set(this.path, update); + __fsState.pendingUpdates.set(this.path, update); void this.safeWrite(dataFetcher(), options).then(() => this.finishUpdate()); } checkNextUpdate() { - const pendingUpdate = pendingUpdates.get(this.path); + const pendingUpdate = __fsState.pendingUpdates.get(this.path); if (!pendingUpdate) throw new Error(`FS: Pending update not found`); if (pendingUpdate.isWriting) throw new Error(`FS: Conflicting update`); const {pendingDataFetcher: dataFetcher, pendingOptions: options} = pendingUpdate; if (!dataFetcher || !options) { // no pending update - pendingUpdates.delete(this.path); + __fsState.pendingUpdates.delete(this.path); return; } this.writeUpdateNow(dataFetcher, options); } finishUpdate() { - const pendingUpdate = pendingUpdates.get(this.path); + const pendingUpdate = __fsState.pendingUpdates.get(this.path); if (!pendingUpdate) throw new Error(`FS: Pending update not found`); if (!pendingUpdate.isWriting) throw new Error(`FS: Conflicting update`); @@ -491,4 +507,3 @@ function getFs(path: string) { export const FS = Object.assign(getFs, { FileReadStream, }); - diff --git a/server/chat-plugins/helptickets.ts b/server/chat-plugins/helptickets.ts index cf35eb8ddf..1ec3b9e8f5 100644 --- a/server/chat-plugins/helptickets.ts +++ b/server/chat-plugins/helptickets.ts @@ -55,7 +55,7 @@ try { export function writeTickets() { FS(TICKET_FILE).writeUpdate( - () => JSON.stringify(tickets) + () => JSON.stringify(tickets), {throttle: 5000} ); } diff --git a/server/punishments.ts b/server/punishments.ts index c7c9f6040e..ba767a0ae9 100644 --- a/server/punishments.ts +++ b/server/punishments.ts @@ -286,7 +286,7 @@ export const Punishments = new class { buf += Punishments.renderEntry(entry, id); } return buf; - }); + }, {throttle: 5000}); } saveRoomPunishments() { @@ -302,7 +302,7 @@ export const Punishments = new class { buf += Punishments.renderEntry(entry, id); } return buf; - }); + }, {throttle: 5000}); } getEntry(entryId: string) { diff --git a/server/rooms.ts b/server/rooms.ts index 247e9aba67..580f09791b 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1143,7 +1143,7 @@ export class GlobalRoomState { JSON.stringify(this.settingsList) .replace(/\{"title":/g, '\n{"title":') .replace(/\]$/, '\n]') - )); + ), {throttle: 5000}); } writeNumRooms() {