1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-12 08:54:00 +02:00
joplin/packages/app-desktop/services/plugins/BackOffHandler.ts
2023-07-27 16:08:11 +01:00

81 lines
3.0 KiB
TypeScript

import Logger from '@joplin/utils/Logger';
import time from '@joplin/lib/time';
const logger = Logger.create('BackOffHandler');
// This handler performs two checks:
//
// 1. If the plugin makes many API calls one after the other, a delay is going
// to be applied before responding. The delay is set using backOffIntervals_.
// When a plugin needs to be throttled that way a warning is displayed so
// that the author gets an opportunity to fix it.
//
// 2. If the plugin makes many simultaneous calls, the handler throws an
// exception to stop the plugin. In that case the plugin will be broken, but
// most plugins will not get this error anyway because call are usually made
// in sequence. It might reveal a bug though - for example if the plugin
// makes a call every 1 second, but does not wait for the response (or assume
// the response will come in less than one second). In that case, the back
// off intervals combined with the incorrect code will make the plugin fail.
export default class BackOffHandler {
// The current logic is:
//
// - Up to 1000 calls per 10 seconds without restrictions
// - For calls 1000 to 2000, a 100 ms wait time is applied
// - Over 2000 calls, a 200 ms wait time is applied
// - After 10 seconds without making any call, the limits are reset (back to
// 0 second between calls).
//
// If more than 5000 simultaneous calls are being throttled, it's a bug in
// the plugin (not waiting for API responses), so we stop responding and
// throw an error.
private backOffIntervals_ =
Array(1000).fill(0).concat(
Array(1000).fill(100)).concat(
[200]);
private lastRequestTime_ = 0;
private pluginId_: string;
private resetBackOffInterval_ = 10 * 1000;
private backOffIndex_ = 0;
private waitCount_ = 0;
private maxWaitCount_ = 5000;
public constructor(pluginId: string) {
this.pluginId_ = pluginId;
}
private backOffInterval() {
const now = Date.now();
if (now - this.lastRequestTime_ > this.resetBackOffInterval_) {
this.backOffIndex_ = 0;
} else {
this.backOffIndex_++;
}
this.lastRequestTime_ = now;
const effectiveIndex = this.backOffIndex_ >= this.backOffIntervals_.length ? this.backOffIntervals_.length - 1 : this.backOffIndex_;
return this.backOffIntervals_[effectiveIndex];
}
public async wait(path: string, args: any) {
const interval = this.backOffInterval();
if (!interval) return;
this.waitCount_++;
logger.warn(`Plugin ${this.pluginId_}: Applying a backoff of ${interval} milliseconds due to frequent plugin API calls. Consider reducing the number of calls, caching the data, or requesting more data per call. API call was: `, path, args, `[Wait count: ${this.waitCount_}]`);
if (this.waitCount_ > this.maxWaitCount_) throw new Error(`Plugin ${this.pluginId_}: More than ${this.maxWaitCount_} API calls are waiting - aborting. Please consider queuing the API calls in your plugins to reduce the load on the application.`);
await time.msleep(interval);
this.waitCount_--;
}
}