From 13b374a927f58abd84b39ea2c13c7df3e00efb0b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 5 Mar 2025 14:59:26 +0100 Subject: [PATCH] fix(Controller): Cap exponential backoff at 1h fixes #1874 Signed-off-by: Marcel Klehr --- src/lib/browser/BrowserController.js | 27 +++++++++++++++++++-- src/lib/native/NativeController.js | 35 ++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/lib/browser/BrowserController.js b/src/lib/browser/BrowserController.js index 67257bf6b3..dd0c1a9f2f 100644 --- a/src/lib/browser/BrowserController.js +++ b/src/lib/browser/BrowserController.js @@ -11,9 +11,10 @@ import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '. import * as Sentry from '@sentry/browser' const INACTIVITY_TIMEOUT = 7 * 1000 // 7 seconds +const MAX_BACKOFF_INTERVAL = 1000 * 60 * 60 // 1 hour const DEFAULT_SYNC_INTERVAL = 15 // 15 minutes const STALE_SYNC_TIME = 1000 * 60 * 60 * 24 * 2 // two days -const INTERVENTION_INTERVAL = 1000 * 60 * 60 * 25 * 91 // 91 days +const INTERVENTION_INTERVAL = 1000 * 60 * 60 * 24 * 91 // 91 days class AlarmManager { constructor(ctl) { @@ -33,8 +34,9 @@ class AlarmManager { continue } if (data.error && data.errorCount > 1) { - if (Date.now() > interval * 2 ** data.errorCount + lastSync) { + if (Date.now() > this.getBackoffInterval(interval, data.errorCount, lastSync) + lastSync) { promises.push(this.ctl.scheduleSync(accountId)) + continue } continue } @@ -47,6 +49,27 @@ class AlarmManager { } await Promise.all(promises) } + + /** + * Calculates the backoff interval based on the synchronization interval and the error count. + * + * This method determines the delay before retrying a synchronization + * after one or more errors have occurred. It uses an exponential + * backoff algorithm with a cap at the maximum backoff interval. + * + * @param {number} interval - The synchronization interval in minutes. + * @param {number} errorCount - The number of consecutive errors encountered. + * @param {number} lastSync - The timestamp of when the last successful sync happened. + * @returns {number} - The calculated backoff interval in milliseconds. + */ + getBackoffInterval(interval, errorCount, lastSync) { + const maxErrorCount = Math.log2(MAX_BACKOFF_INTERVAL / (interval * 1000 * 60)) + if (errorCount < maxErrorCount || lastSync + MAX_BACKOFF_INTERVAL > Date.now()) { + return Math.min(MAX_BACKOFF_INTERVAL, interval * 1000 * 60 * Math.pow(2, errorCount)) + } else { + return MAX_BACKOFF_INTERVAL + MAX_BACKOFF_INTERVAL * (errorCount - maxErrorCount) + } + } } export default class BrowserController { diff --git a/src/lib/native/NativeController.js b/src/lib/native/NativeController.js index d1688ba0c4..4e4a9c8571 100644 --- a/src/lib/native/NativeController.js +++ b/src/lib/native/NativeController.js @@ -6,6 +6,7 @@ import Account from '../Account' import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '../interfaces/Controller' const INACTIVITY_TIMEOUT = 1000 * 7 +const MAX_BACKOFF_INTERVAL = 1000 * 60 * 60 // 1 hour const DEFAULT_SYNC_INTERVAL = 15 class AlarmManager { @@ -31,18 +32,48 @@ class AlarmManager { for (let accountId of accounts) { const account = await Account.get(accountId) const data = account.getData() + const lastSync = data.lastSync || 0 + const interval = data.syncInterval || DEFAULT_SYNC_INTERVAL if (data.scheduled) { this.ctl.scheduleSync(accountId) + continue + } + if (data.error && data.errorCount > 1) { + if (Date.now() > this.getBackoffInterval(interval, data.errorCount, lastSync) + lastSync) { + this.ctl.scheduleSync(accountId) + continue + } + continue } if ( - !data.lastSync || Date.now() > - (data.syncInterval || DEFAULT_SYNC_INTERVAL) * 1000 * 60 + data.lastSync + interval * 1000 * 60 + data.lastSync ) { this.ctl.scheduleSync(accountId) } } } + + /** + * Calculates the backoff interval based on the synchronization interval and the error count. + * + * This method determines the delay before retrying a synchronization + * after one or more errors have occurred. It uses an exponential + * backoff algorithm with a cap at the maximum backoff interval. + * + * @param {number} interval - The synchronization interval in minutes. + * @param {number} errorCount - The number of consecutive errors encountered. + * @param {number} lastSync - The timestamp of when the last successful sync happened. + * @returns {number} - The calculated backoff interval in milliseconds. + */ + getBackoffInterval(interval, errorCount, lastSync) { + const maxErrorCount = Math.log2(MAX_BACKOFF_INTERVAL / (interval * 1000 * 60)) + if (errorCount < maxErrorCount || lastSync + MAX_BACKOFF_INTERVAL > Date.now()) { + return Math.min(MAX_BACKOFF_INTERVAL, interval * 1000 * 60 * Math.pow(2, errorCount)) + } else { + return MAX_BACKOFF_INTERVAL + MAX_BACKOFF_INTERVAL * (errorCount - maxErrorCount) + } + } } export default class NativeController {