2021-12-27 20:12:21 +02:00
import Logger from '@joplin/lib/Logger' ;
2022-04-19 16:52:32 +02:00
import time from '@joplin/lib/time' ;
2021-12-27 20:12:21 +02:00
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.
//
2022-04-19 16:52:32 +02:00
// 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
2021-12-27 20:12:21 +02:00
// 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 {
2022-04-19 16:52:32 +02:00
// The current logic is:
//
2022-04-27 14:18:31 +02:00
// - 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
2022-04-19 16:52:32 +02:00
// - After 10 seconds without making any call, the limits are reset (back to
// 0 second between calls).
//
2022-04-27 14:18:31 +02:00
// 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.
2022-04-19 16:52:32 +02:00
private backOffIntervals_ =
2022-04-27 14:18:31 +02:00
Array ( 1000 ) . fill ( 0 ) . concat (
Array ( 1000 ) . fill ( 100 ) ) . concat (
[ 200 ] ) ;
2022-04-19 16:52:32 +02:00
2021-12-27 20:12:21 +02:00
private lastRequestTime_ = 0 ;
private pluginId_ : string ;
2022-04-27 14:18:31 +02:00
private resetBackOffInterval_ = 10 * 1000 ;
2021-12-27 20:12:21 +02:00
private backOffIndex_ = 0 ;
private waitCount_ = 0 ;
2022-04-27 14:18:31 +02:00
private maxWaitCount_ = 5000 ;
2021-12-27 20:12:21 +02:00
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_ ++ ;
2022-04-27 14:18:31 +02:00
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_ } ] ` ) ;
2021-12-27 20:12:21 +02:00
2022-04-19 16:52:32 +02:00
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. ` ) ;
2022-01-19 11:02:53 +02:00
2022-04-27 14:18:31 +02:00
await time . msleep ( interval ) ;
2022-01-19 11:02:53 +02:00
2022-04-19 16:52:32 +02:00
this . waitCount_ -- ;
2021-12-27 20:12:21 +02:00
}
}