-
Notifications
You must be signed in to change notification settings - Fork 0
/
withExponentialBackoff.mjs
114 lines (90 loc) · 3.89 KB
/
withExponentialBackoff.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
function isUsageRateError(err){
return Array.isArray(err.errors) && err.errors[0].domain === "usageLimits";
}
const RETRY_MINIMUM_DELAY = 25;
const MAX_PENDING = 3;
let queue;
function retry(){
//console.log('pending call', queue.toRetryCalls.size, queue.pendingCalls.size, queue.delay);
// extracting 2 elements to retry. This grows exponentially the number of parallel calls after a reset
const [tr1, tr2] = queue.toRetryCalls;
[tr1, tr2].forEach(toRetryCall => {
if(!toRetryCall || queue.pendingCalls.size >= MAX_PENDING) // there was only one left
return;
const { f, args, resolve, reject } = toRetryCall;
queue.toRetryCalls.delete(toRetryCall);
queue.pendingCalls.add(toRetryCall);
f(...args)
.then(result => {
//console.log('successful retry', queue.toRetryCalls.size, queue.pendingCalls.size, queue.delay)
if(queue.toRetryCalls.size + queue.pendingCalls.size === 0){
queue = undefined;
}
else{
queue.delay = RETRY_MINIMUM_DELAY;
queue.timeouts.add(setTimeout(retry, queue.delay));
}
resolve(result);
queue.pendingCalls.delete(toRetryCall);
queue.toRetryCalls.delete(toRetryCall);
})
.catch(err => {
if(isUsageRateError(err)){
//console.log('unsuccessful retry', queue.toRetryCalls.size, queue.pendingCalls.size, queue.delay);
if(queue.pendingCalls.has(toRetryCall)){
// reset queue
queue.timeouts.forEach(t => clearTimeout(t));
queue.timeouts = new Set();
queue.pendingCalls.forEach(c => {
queue.toRetryCalls.add(c);
queue.pendingCalls.delete(c);
});
queue.delay *= 2;
queue.timeouts.add(setTimeout(retry, queue.delay));
}
}
else{ // not a usage limit issue, forward
reject(err);
queue.pendingCalls.delete(toRetryCall);
queue.toRetryCalls.delete(toRetryCall);
}
})
})
}
export default function withExponentialBackoff(f){
// returns a function which does the same thing than f, but retries with exponential backoff
// https://developers.google.com/drive/v3/web/handle-errors?hl=fr#exponential-backoff
return function(...args){
if(queue){
// already waiting, let's not make things worse and add to queue
return new Promise((resolve, reject) => {
queue.toRetryCalls.add({ f, args, resolve, reject });
})
}
else{
return f(...args)
.catch(err => {
if(isUsageRateError(err)){
//console.log('usage limit error, backing off')
return new Promise((resolve, reject) => {
const toRetryCall = { f, args, resolve, reject };
// may have been created in the meantime
if(!queue){
queue = {
delay: RETRY_MINIMUM_DELAY,
toRetryCalls: new Set(),
pendingCalls: new Set(),
timeouts: new Set()
}
queue.timeouts.add(setTimeout(retry, queue.delay));
}
queue.toRetryCalls.add(toRetryCall);
});
}
else{ // not a usage limit issue, forward
throw err;
}
})
}
}
}