Skip to content

Commit

Permalink
Merge pull request #1876 from floccusaddon/fix/bounded-exponential-ba…
Browse files Browse the repository at this point in the history
…ckoff

fix: Cap exponential backoff at 1h
  • Loading branch information
marcelklehr authored Mar 6, 2025
2 parents 05aae10 + 13b374a commit bebc42b
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 4 deletions.
27 changes: 25 additions & 2 deletions src/lib/browser/BrowserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
35 changes: 33 additions & 2 deletions src/lib/native/NativeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit bebc42b

Please sign in to comment.