mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-12 08:54:00 +02:00
81 lines
3.0 KiB
TypeScript
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_--;
|
|
}
|
|
|
|
}
|