summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-04-21 12:58:21 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-04-21 12:58:21 +0200
commit171530b04a5fdbc8c27fa4f29ee37a98545ab335 (patch)
tree09fd98542d83e4da1506c6ebfbf8c6fe21618da1 /gui/src
parent0e13ac8bf009d6fabfb9039f845791e9b233b856 (diff)
parent513b78e1f665f930397a123764de2b48309da3e3 (diff)
downloadmullvadvpn-171530b04a5fdbc8c27fa4f29ee37a98545ab335.tar.xz
mullvadvpn-171530b04a5fdbc8c27fa4f29ee37a98545ab335.zip
Merge branch 'prevent-account-data-cache-parallel-fetch'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/account-data-cache.ts82
-rw-r--r--gui/src/main/index.ts6
-rw-r--r--gui/src/shared/scheduler.ts12
3 files changed, 67 insertions, 33 deletions
diff --git a/gui/src/main/account-data-cache.ts b/gui/src/main/account-data-cache.ts
index 7db2c05ca8..ac829ca0f3 100644
--- a/gui/src/main/account-data-cache.ts
+++ b/gui/src/main/account-data-cache.ts
@@ -2,16 +2,14 @@ import log from 'electron-log';
import moment from 'moment';
import { AccountToken, IAccountData } from '../shared/daemon-rpc-types';
import consumePromise from '../shared/promise';
+import { Scheduler } from '../shared/scheduler';
+import { InvalidAccountError } from './errors';
const EXPIRED_ACCOUNT_REFRESH_PERIOD = 60_000;
-export enum AccountFetchRetryAction {
- stop,
- retry,
-}
interface IAccountFetchWatcher {
onFinish: () => void;
- onError: (error: Error) => AccountFetchRetryAction;
+ onError: (error: Error) => void;
}
// An account data cache that helps to throttle RPC requests to get_account_data and retain the
@@ -19,8 +17,9 @@ interface IAccountFetchWatcher {
export default class AccountDataCache {
private currentAccount?: AccountToken;
private expiresAt?: Date;
- private fetchAttempt = 0;
- private fetchRetryTimeout?: NodeJS.Timeout;
+ private performingFetch = false;
+ private waitStrategy = new WaitStrategy();
+ private fetchRetryScheduler = new Scheduler();
private watchers: IAccountFetchWatcher[] = [];
constructor(
@@ -35,25 +34,30 @@ export default class AccountDataCache {
this.currentAccount = accountToken;
}
- // Only fetch is value has expired
+ // Only fetch if value has expired
if (this.isExpired()) {
if (watcher) {
this.watchers.push(watcher);
}
- consumePromise(this.performFetch(accountToken));
+ this.fetchRetryScheduler.cancel();
+ // If a scheduled retry is cancelled the fetchAttempt shouldn't be increased.
+ this.waitStrategy.decrease();
+
+ // Only fetch if there's no fetch for this account number in progress.
+ if (!this.performingFetch) {
+ consumePromise(this.performFetch(accountToken));
+ }
} else if (watcher) {
watcher.onFinish();
}
}
public invalidate() {
- if (this.fetchRetryTimeout) {
- clearTimeout(this.fetchRetryTimeout);
- this.fetchRetryTimeout = undefined;
- this.fetchAttempt = 0;
- }
+ this.fetchRetryScheduler.cancel();
+ this.waitStrategy.reset();
+ this.performingFetch = false;
this.expiresAt = undefined;
this.updateHandler();
this.notifyWatchers((watcher) => {
@@ -72,6 +76,7 @@ export default class AccountDataCache {
}
private async performFetch(accountToken: AccountToken) {
+ this.performingFetch = true;
try {
// it's possible for invalidate() to be called or for a fetch for a different account token
// to start before this fetch completes, so checking if the current account token is the one
@@ -81,10 +86,13 @@ export default class AccountDataCache {
if (this.currentAccount === accountToken) {
this.setValue(accountData);
this.scheduleRefetchIfExpired(accountToken, accountData);
+ this.waitStrategy.reset();
+ this.performingFetch = false;
}
} catch (error) {
if (this.currentAccount === accountToken) {
this.handleFetchError(accountToken, error);
+ this.performingFetch = false;
}
}
}
@@ -97,24 +105,15 @@ export default class AccountDataCache {
}
private handleFetchError(accountToken: AccountToken, error: Error) {
- let shouldRetry = true;
-
- this.notifyWatchers((watcher) => {
- if (watcher.onError(error) === AccountFetchRetryAction.stop) {
- shouldRetry = false;
- }
- });
-
- if (shouldRetry) {
+ this.notifyWatchers((w) => w.onError(error));
+ if (!(error instanceof InvalidAccountError)) {
this.scheduleRetry(accountToken);
}
}
private scheduleRetry(accountToken: AccountToken) {
- this.fetchAttempt += 1;
-
- // Max delay: 2^11 = 2048
- const delay = Math.pow(2, Math.min(this.fetchAttempt + 2, 11)) * 1000;
+ this.waitStrategy.increase();
+ const delay = this.waitStrategy.delay();
log.warn(`Failed to fetch account data. Retrying in ${delay} ms`);
@@ -122,8 +121,7 @@ export default class AccountDataCache {
}
private scheduleFetch(accountToken: AccountToken, delay: number) {
- this.fetchRetryTimeout = global.setTimeout(() => {
- this.fetchRetryTimeout = undefined;
+ this.fetchRetryScheduler.schedule(() => {
consumePromise(this.performFetch(accountToken));
}, delay);
}
@@ -132,3 +130,29 @@ export default class AccountDataCache {
this.watchers.splice(0).forEach(notify);
}
}
+
+const MAX_ATTEMPT = 9;
+
+class WaitStrategy {
+ private counter = 0;
+
+ public increase() {
+ if (this.counter < MAX_ATTEMPT) {
+ this.counter += 1;
+ }
+ }
+ public decrease() {
+ if (this.counter > 0) {
+ this.counter -= 1;
+ }
+ }
+
+ public reset() {
+ this.counter = 0;
+ }
+
+ public delay(): number {
+ // Max delay: 2^11 = 2048
+ return Math.pow(2, this.counter + 2) * 1000;
+ }
+}
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 61b689456c..6f9185688a 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -36,7 +36,7 @@ import {
setupLogging,
} from '../shared/logging';
import consumePromise from '../shared/promise';
-import AccountDataCache, { AccountFetchRetryAction } from './account-data-cache';
+import AccountDataCache from './account-data-cache';
import { getOpenAtLogin, setOpenAtLogin } from './autostart';
import {
ConnectionObserver,
@@ -1139,13 +1139,11 @@ class ApplicationMain {
this.accountDataCache.invalidate();
this.accountDataCache.fetch(accountToken, {
onFinish: () => resolve({ status: 'verified' }),
- onError: (error): AccountFetchRetryAction => {
+ onError: (error) => {
if (error instanceof InvalidAccountError) {
reject(error);
- return AccountFetchRetryAction.stop;
} else {
resolve({ status: 'deferred', error });
- return AccountFetchRetryAction.retry;
}
},
});
diff --git a/gui/src/shared/scheduler.ts b/gui/src/shared/scheduler.ts
new file mode 100644
index 0000000000..af28a7e95b
--- /dev/null
+++ b/gui/src/shared/scheduler.ts
@@ -0,0 +1,12 @@
+export class Scheduler {
+ private timer?: number;
+
+ public schedule(action: () => void, delay: number) {
+ this.cancel();
+ this.timer = setTimeout(action, delay);
+ }
+
+ public cancel() {
+ clearTimeout(this.timer);
+ }
+}