From cfb68e5725febfe6961587c177d9150c66fa3955 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 27 Feb 2022 02:55:35 -0500 Subject: [PATCH] pad_utils: Rate limit identical `warnDeprecated` calls --- src/static/js/pad_utils.js | 15 +++++++++++++++ src/tests/backend/specs/pad_utils.js | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 828e62567..4a33ef449 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -93,6 +93,9 @@ const padutils = { * Prints a warning message followed by a stack trace (to make it easier to figure out what code * is using the deprecated function). * + * Identical deprecation warnings (as determined by the stack trace, if available) are rate + * limited to avoid log spam. + * * Most browsers include UI widget to examine the stack at the time of the warning, but this * includes the stack in the log message for a couple of reasons: * - This makes it possible to see the stack if the code runs in Node.js. @@ -106,6 +109,18 @@ const padutils = { const err = new Error(); if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated); err.name = ''; + // Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam. + if (typeof err.stack === 'string') { + if (padutils.warnDeprecated._rl == null) { + padutils.warnDeprecated._rl = + {prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000}; + } + const rl = padutils.warnDeprecated._rl; + const now = rl.now(); + const prev = rl.prevs.get(err.stack); + if (prev != null && now - prev < rl.period) return; + rl.prevs.set(err.stack, now); + } if (err.stack) args.push(err.stack); (padutils.warnDeprecated.logger || console).warn(...args); }, diff --git a/src/tests/backend/specs/pad_utils.js b/src/tests/backend/specs/pad_utils.js index 51cc85f1e..b4e815187 100644 --- a/src/tests/backend/specs/pad_utils.js +++ b/src/tests/backend/specs/pad_utils.js @@ -14,6 +14,7 @@ describe(__filename, function () { afterEach(async function () { warnDeprecated.logger = backups.logger; + delete warnDeprecated._rl; // Reset internal rate limiter state. }); it('includes the stack', async function () { @@ -22,5 +23,21 @@ describe(__filename, function () { warnDeprecated(); assert(got.includes(__filename)); }); + + it('rate limited', async function () { + let got = 0; + warnDeprecated.logger = {warn: () => ++got}; + warnDeprecated(); // Initialize internal rate limiter state. + const {period} = warnDeprecated._rl; + got = 0; + const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]]; + for (const [now, want] of testCases) { // In a loop so that the stack trace is the same. + warnDeprecated._rl.now = () => now; + warnDeprecated(); + assert.equal(got, want); + } + warnDeprecated(); // Should have a different stack trace. + assert.equal(got, testCases[testCases.length - 1][1] + 1); + }); }); });