Skip to content

Commit df20156

Browse files
committed
Refactor autosync service to be more reliable and predictable
1 parent 074fc4d commit df20156

3 files changed

Lines changed: 104 additions & 77 deletions

File tree

src/vs/platform/userDataSync/common/userDataAutoSyncService.ts

Lines changed: 102 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { timeout, Delayer } from 'vs/base/common/async';
6+
import { Delayer, disposableTimeout } from 'vs/base/common/async';
77
import { Event, Emitter } from 'vs/base/common/event';
8-
import { Disposable } from 'vs/base/common/lifecycle';
8+
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
99
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService, ALL_SYNC_RESOURCES } from 'vs/platform/userDataSync/common/userDataSync';
1010
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
1111
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -20,10 +20,10 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
2020

2121
_serviceBrand: any;
2222

23-
private enabled: boolean = this.getDefaultEnablementValue();
23+
private readonly autoSync = this._register(new MutableDisposable<AutoSync>());
2424
private successiveFailures: number = 0;
2525
private lastSyncTriggerTime: number | undefined = undefined;
26-
private readonly syncDelayer: Delayer<void>;
26+
private readonly syncTriggerDelayer: Delayer<void>;
2727

2828
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
2929
readonly onError: Event<UserDataSyncError> = this._onError.event;
@@ -36,77 +36,31 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
3636
@ITelemetryService private readonly telemetryService: ITelemetryService,
3737
) {
3838
super();
39-
this.updateEnablement(false, true);
40-
this.syncDelayer = this._register(new Delayer<void>(0));
41-
this._register(Event.any<any>(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
42-
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
43-
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false)));
39+
this.updateEnablement();
40+
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
41+
this._register(Event.any(authTokenService.onDidChangeToken, userDataSyncService.onDidChangeStatus, this.userDataSyncEnablementService.onDidChangeEnablement)(() => this.updateEnablement()));
4442
this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerAutoSync([RESOURCE_ENABLEMENT_SOURCE])));
4543
}
4644

47-
// For tests purpose only
48-
protected getDefaultEnablementValue(): boolean { return false; }
49-
50-
private updateEnablement(stopIfDisabled: boolean, auto: boolean): void {
45+
private updateEnablement(): void {
5146
const { enabled, reason } = this.isAutoSyncEnabled();
52-
if (this.enabled === enabled) {
53-
return;
54-
}
55-
56-
this.enabled = enabled;
57-
if (this.enabled) {
58-
this.logService.info('Auto Sync: Started');
59-
this.sync(true, auto);
60-
return;
47+
if (enabled) {
48+
if (this.autoSync.value === undefined) {
49+
const autoSync = new AutoSync(this.startAutoSync(), 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService);
50+
autoSync.register(autoSync.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
51+
autoSync.register(autoSync.onDidFinishSync(e => this.onDidFinishSync(e)));
52+
this.autoSync.value = autoSync;
53+
}
6154
} else {
62-
this.resetFailures();
63-
if (stopIfDisabled) {
64-
this.userDataSyncService.stop();
65-
this.logService.info('Auto Sync: stopped because', reason);
55+
if (this.autoSync.value !== undefined) {
56+
this.logService.trace('Auto Sync: Disabled because', reason);
57+
this.autoSync.clear();
6658
}
6759
}
68-
6960
}
7061

71-
private async sync(loop: boolean, auto: boolean): Promise<void> {
72-
if (this.enabled) {
73-
try {
74-
this.lastSyncTriggerTime = new Date().getTime();
75-
await this.userDataSyncService.sync();
76-
this.resetFailures();
77-
} catch (e) {
78-
const error = UserDataSyncError.toUserDataSyncError(e);
79-
if (error.code === UserDataSyncErrorCode.TurnedOff || error.code === UserDataSyncErrorCode.SessionExpired) {
80-
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
81-
this.logService.info('Auto Sync: Resetting the local sync state.');
82-
await this.userDataSyncService.resetLocal();
83-
this.logService.info('Auto Sync: Completed resetting the local sync state.');
84-
if (auto) {
85-
this.userDataSyncEnablementService.setEnablement(false);
86-
this._onError.fire(error);
87-
return;
88-
} else {
89-
return this.sync(loop, auto);
90-
}
91-
}
92-
if (error.code === UserDataSyncErrorCode.TooManyRequests) {
93-
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
94-
this.userDataSyncEnablementService.setEnablement(false);
95-
this._onError.fire(error);
96-
return;
97-
}
98-
this.logService.error(error);
99-
this.successiveFailures++;
100-
this._onError.fire(error);
101-
}
102-
if (loop) {
103-
await timeout(1000 * 60 * 5);
104-
this.sync(loop, true);
105-
}
106-
} else {
107-
this.logService.trace('Auto Sync: Not syncing as it is disabled.');
108-
}
109-
}
62+
// For tests purpose only
63+
protected startAutoSync(): boolean { return true; }
11064

11165
private isAutoSyncEnabled(): { enabled: boolean, reason?: string } {
11266
if (!this.userDataSyncEnablementService.isEnabled()) {
@@ -121,14 +75,35 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
12175
return { enabled: true };
12276
}
12377

124-
private resetFailures(): void {
125-
this.successiveFailures = 0;
78+
private async onDidFinishSync(error: Error | undefined): Promise<void> {
79+
if (!error) {
80+
// Sync finished without errors
81+
this.successiveFailures = 0;
82+
return;
83+
}
84+
85+
// Error while syncing
86+
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
87+
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
88+
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
89+
await this.userDataSyncService.resetLocal();
90+
this.logService.info('Auto Sync: Did reset the local sync state.');
91+
this.userDataSyncEnablementService.setEnablement(false);
92+
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
93+
} else if (userDataSyncError.code === UserDataSyncErrorCode.TooManyRequests) {
94+
this.userDataSyncEnablementService.setEnablement(false);
95+
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
96+
} else {
97+
this.logService.error(userDataSyncError);
98+
this.successiveFailures++;
99+
}
100+
this._onError.fire(userDataSyncError);
126101
}
127102

128103
private sources: string[] = [];
129104
async triggerAutoSync(sources: string[]): Promise<void> {
130-
if (!this.enabled) {
131-
return this.syncDelayer.cancel();
105+
if (this.autoSync.value === undefined) {
106+
return this.syncTriggerDelayer.cancel();
132107
}
133108

134109
/*
@@ -143,16 +118,68 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
143118
}
144119

145120
this.sources.push(...sources);
146-
return this.syncDelayer.trigger(() => {
121+
return this.syncTriggerDelayer.trigger(async () => {
147122
this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources });
148123
this.sources = [];
149-
150-
this.logService.info('Auto Sync: Triggered.');
151-
return this.sync(false, true);
124+
if (this.autoSync.value) {
125+
await this.autoSync.value.sync('Activity');
126+
}
152127
}, this.successiveFailures
153128
? 1000 * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
154-
: 0); /* Do not delay if there are no failures */
129+
: 1000); /* Debounce for a second if there are no failures */
130+
131+
}
132+
133+
}
134+
135+
class AutoSync extends Disposable {
136+
137+
private static readonly INTERVAL_SYNCING = 'Interval';
138+
139+
private readonly intervalHandler = this._register(new MutableDisposable<IDisposable>());
140+
141+
private readonly _onDidStartSync = this._register(new Emitter<void>());
142+
readonly onDidStartSync = this._onDidStartSync.event;
143+
144+
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
145+
readonly onDidFinishSync = this._onDidFinishSync.event;
146+
147+
constructor(
148+
start: boolean,
149+
private readonly interval: number /* in milliseconds */,
150+
private readonly userDataSyncService: IUserDataSyncService,
151+
private readonly logService: IUserDataSyncLogService,
152+
) {
153+
super();
154+
if (start) {
155+
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
156+
this._register(toDisposable(() => {
157+
this.logService.info('Auto Sync: Stopped');
158+
this.userDataSyncService.stop();
159+
}));
160+
this.logService.info('Auto Sync: Started');
161+
this.sync(AutoSync.INTERVAL_SYNCING);
162+
}
163+
}
164+
165+
private waitUntilNextIntervalAndSync(): void {
166+
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval);
167+
}
168+
169+
async sync(reason: string): Promise<void> {
170+
this.logService.info(`Auto Sync: Triggered by ${reason}`);
171+
this._onDidStartSync.fire();
172+
let error: Error | undefined;
173+
try {
174+
await this.userDataSyncService.sync();
175+
} catch (e) {
176+
error = e;
177+
}
178+
this._onDidFinishSync.fire(error);
179+
}
155180

181+
register<T extends IDisposable>(t: T): T {
182+
return super._register(t);
156183
}
157184

158185
}

src/vs/platform/userDataSync/common/userDataSyncService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
257257
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
258258
for (const synchroniser of this.synchronisers) {
259259
try {
260-
synchroniser.resetLocal();
260+
await synchroniser.resetLocal();
261261
} catch (e) {
262262
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
263263
this.logService.error(e);

src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDat
1010
import { IUserDataSyncService, SyncResource, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
1111

1212
class TestUserDataAutoSyncService extends UserDataAutoSyncService {
13-
protected getDefaultEnablementValue(): boolean { return true; }
13+
protected startAutoSync(): boolean { return false; }
1414
}
1515

1616
suite('UserDataAutoSyncService', () => {

0 commit comments

Comments
 (0)