Skip to content

Commit bebc42b

Browse files
authored
Merge pull request #1876 from floccusaddon/fix/bounded-exponential-backoff
fix: Cap exponential backoff at 1h
2 parents 05aae10 + 13b374a commit bebc42b

File tree

2 files changed

+58
-4
lines changed

2 files changed

+58
-4
lines changed

src/lib/browser/BrowserController.js

+25-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '.
1111
import * as Sentry from '@sentry/browser'
1212

1313
const INACTIVITY_TIMEOUT = 7 * 1000 // 7 seconds
14+
const MAX_BACKOFF_INTERVAL = 1000 * 60 * 60 // 1 hour
1415
const DEFAULT_SYNC_INTERVAL = 15 // 15 minutes
1516
const STALE_SYNC_TIME = 1000 * 60 * 60 * 24 * 2 // two days
16-
const INTERVENTION_INTERVAL = 1000 * 60 * 60 * 25 * 91 // 91 days
17+
const INTERVENTION_INTERVAL = 1000 * 60 * 60 * 24 * 91 // 91 days
1718

1819
class AlarmManager {
1920
constructor(ctl) {
@@ -33,8 +34,9 @@ class AlarmManager {
3334
continue
3435
}
3536
if (data.error && data.errorCount > 1) {
36-
if (Date.now() > interval * 2 ** data.errorCount + lastSync) {
37+
if (Date.now() > this.getBackoffInterval(interval, data.errorCount, lastSync) + lastSync) {
3738
promises.push(this.ctl.scheduleSync(accountId))
39+
continue
3840
}
3941
continue
4042
}
@@ -47,6 +49,27 @@ class AlarmManager {
4749
}
4850
await Promise.all(promises)
4951
}
52+
53+
/**
54+
* Calculates the backoff interval based on the synchronization interval and the error count.
55+
*
56+
* This method determines the delay before retrying a synchronization
57+
* after one or more errors have occurred. It uses an exponential
58+
* backoff algorithm with a cap at the maximum backoff interval.
59+
*
60+
* @param {number} interval - The synchronization interval in minutes.
61+
* @param {number} errorCount - The number of consecutive errors encountered.
62+
* @param {number} lastSync - The timestamp of when the last successful sync happened.
63+
* @returns {number} - The calculated backoff interval in milliseconds.
64+
*/
65+
getBackoffInterval(interval, errorCount, lastSync) {
66+
const maxErrorCount = Math.log2(MAX_BACKOFF_INTERVAL / (interval * 1000 * 60))
67+
if (errorCount < maxErrorCount || lastSync + MAX_BACKOFF_INTERVAL > Date.now()) {
68+
return Math.min(MAX_BACKOFF_INTERVAL, interval * 1000 * 60 * Math.pow(2, errorCount))
69+
} else {
70+
return MAX_BACKOFF_INTERVAL + MAX_BACKOFF_INTERVAL * (errorCount - maxErrorCount)
71+
}
72+
}
5073
}
5174

5275
export default class BrowserController {

src/lib/native/NativeController.js

+33-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Account from '../Account'
66
import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '../interfaces/Controller'
77

88
const INACTIVITY_TIMEOUT = 1000 * 7
9+
const MAX_BACKOFF_INTERVAL = 1000 * 60 * 60 // 1 hour
910
const DEFAULT_SYNC_INTERVAL = 15
1011

1112
class AlarmManager {
@@ -31,18 +32,48 @@ class AlarmManager {
3132
for (let accountId of accounts) {
3233
const account = await Account.get(accountId)
3334
const data = account.getData()
35+
const lastSync = data.lastSync || 0
36+
const interval = data.syncInterval || DEFAULT_SYNC_INTERVAL
3437
if (data.scheduled) {
3538
this.ctl.scheduleSync(accountId)
39+
continue
40+
}
41+
if (data.error && data.errorCount > 1) {
42+
if (Date.now() > this.getBackoffInterval(interval, data.errorCount, lastSync) + lastSync) {
43+
this.ctl.scheduleSync(accountId)
44+
continue
45+
}
46+
continue
3647
}
3748
if (
38-
!data.lastSync ||
3949
Date.now() >
40-
(data.syncInterval || DEFAULT_SYNC_INTERVAL) * 1000 * 60 + data.lastSync
50+
interval * 1000 * 60 + data.lastSync
4151
) {
4252
this.ctl.scheduleSync(accountId)
4353
}
4454
}
4555
}
56+
57+
/**
58+
* Calculates the backoff interval based on the synchronization interval and the error count.
59+
*
60+
* This method determines the delay before retrying a synchronization
61+
* after one or more errors have occurred. It uses an exponential
62+
* backoff algorithm with a cap at the maximum backoff interval.
63+
*
64+
* @param {number} interval - The synchronization interval in minutes.
65+
* @param {number} errorCount - The number of consecutive errors encountered.
66+
* @param {number} lastSync - The timestamp of when the last successful sync happened.
67+
* @returns {number} - The calculated backoff interval in milliseconds.
68+
*/
69+
getBackoffInterval(interval, errorCount, lastSync) {
70+
const maxErrorCount = Math.log2(MAX_BACKOFF_INTERVAL / (interval * 1000 * 60))
71+
if (errorCount < maxErrorCount || lastSync + MAX_BACKOFF_INTERVAL > Date.now()) {
72+
return Math.min(MAX_BACKOFF_INTERVAL, interval * 1000 * 60 * Math.pow(2, errorCount))
73+
} else {
74+
return MAX_BACKOFF_INTERVAL + MAX_BACKOFF_INTERVAL * (errorCount - maxErrorCount)
75+
}
76+
}
4677
}
4778

4879
export default class NativeController {

0 commit comments

Comments
 (0)