Skip to content

Commit f8995e0

Browse files
authored
store last sync data in state (microsoft#166133)
fall back to server if last sync content does not exist
1 parent b982536 commit f8995e0

File tree

8 files changed

+365
-79
lines changed

8 files changed

+365
-79
lines changed

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

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
2323
import { FileChangesEvent, FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files';
2424
import { ILogService } from 'vs/platform/log/common/log';
2525
import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId';
26-
import { IStorageService } from 'vs/platform/storage/common/storage';
26+
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
2727
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2828
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
2929
import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync';
@@ -98,14 +98,20 @@ interface ISyncResourcePreview extends IBaseSyncResourcePreview {
9898
readonly resourcePreviews: IEditableResourcePreview[];
9999
}
100100

101+
interface ILastSyncUserDataState {
102+
readonly ref: string;
103+
readonly version: string | undefined;
104+
[key: string]: any;
105+
}
106+
101107
export abstract class AbstractSynchroniser extends Disposable implements IUserDataSynchroniser {
102108

103109
private syncPreviewPromise: CancelablePromise<ISyncResourcePreview> | null = null;
104110

105111
protected readonly syncFolder: URI;
106112
protected readonly syncPreviewFolder: URI;
107113
protected readonly extUri: IExtUri;
108-
private readonly currentMachineIdPromise: Promise<string>;
114+
protected readonly currentMachineIdPromise: Promise<string>;
109115

110116
private _status: SyncStatus = SyncStatus.Idle;
111117
get status(): SyncStatus { return this._status; }
@@ -122,6 +128,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
122128
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
123129

124130
protected readonly lastSyncResource: URI;
131+
private readonly lastSyncUserDataStateKey = `${this.collection ? `${this.collection}.` : ''}${this.syncResource.syncResource}.lastSyncUserData`;
125132
private hasSyncResourceStateVersionChanged: boolean = false;
126133
protected readonly syncResourceLogLabel: string;
127134

@@ -134,7 +141,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
134141
readonly collection: string | undefined,
135142
@IFileService protected readonly fileService: IFileService,
136143
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
137-
@IStorageService storageService: IStorageService,
144+
@IStorageService protected readonly storageService: IStorageService,
138145
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
139146
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
140147
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -327,7 +334,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
327334
// Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624
328335
remoteUserData = await this.getRemoteUserData(null);
329336

330-
// Get the latest last sync user data. Because multiples parallel syncs (in Web) could share same last sync data
337+
// Get the latest last sync user data. Because multiple parallel syncs (in Web) could share same last sync data
331338
// and one of them successfully updated remote and last sync state.
332339
lastSyncUserData = await this.getLastSyncUserData();
333340

@@ -507,9 +514,12 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
507514
}
508515

509516
async resetLocal(): Promise<void> {
517+
this.storageService.remove(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
510518
try {
511519
await this.fileService.del(this.lastSyncResource);
512-
} catch (e) { /* ignore */ }
520+
} catch (e) {
521+
this.logService.error(e);
522+
}
513523
}
514524

515525
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, token: CancellationToken): Promise<ISyncResourcePreview> {
@@ -558,48 +568,116 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
558568
return { syncResource: this.resource, profile: this.syncResource.profile, remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine };
559569
}
560570

561-
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
562-
try {
563-
const content = await this.fileService.readFile(this.lastSyncResource);
564-
const parsed = JSON.parse(content.value.toString());
565-
const resourceSyncStateVersion = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
566-
this.hasSyncResourceStateVersionChanged = parsed.version && resourceSyncStateVersion && parsed.version !== resourceSyncStateVersion;
567-
if (this.hasSyncResourceStateVersionChanged) {
568-
this.logService.info(`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${parsed.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`);
569-
await this.resetLocal();
570-
return null;
571-
}
571+
async getLastSyncUserData<T = IRemoteUserData & { [key: string]: any }>(): Promise<T | null> {
572+
let storedLastSyncUserDataStateContent = this.storageService.get(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
573+
if (!storedLastSyncUserDataStateContent) {
574+
storedLastSyncUserDataStateContent = await this.migrateLastSyncUserData();
575+
}
572576

573-
const userData: IUserData = parsed as IUserData;
574-
if (userData.content === null) {
575-
return { ref: parsed.ref, syncData: null } as T;
576-
}
577-
const syncData: ISyncData = JSON.parse(userData.content);
577+
// Last Sync Data state does not exist
578+
if (!storedLastSyncUserDataStateContent) {
579+
this.logService.info(`${this.syncResourceLogLabel}: Last sync data state does not exist.`);
580+
return null;
581+
}
578582

579-
/* Check if syncData is of expected type. Return only if matches */
580-
if (isSyncData(syncData)) {
581-
return { ...parsed, ...{ syncData, content: undefined } };
583+
const lastSyncUserDataState: ILastSyncUserDataState = JSON.parse(storedLastSyncUserDataStateContent);
584+
const resourceSyncStateVersion = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
585+
this.hasSyncResourceStateVersionChanged = !!lastSyncUserDataState.version && !!resourceSyncStateVersion && lastSyncUserDataState.version !== resourceSyncStateVersion;
586+
if (this.hasSyncResourceStateVersionChanged) {
587+
this.logService.info(`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${lastSyncUserDataState.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`);
588+
await this.resetLocal();
589+
return null;
590+
}
591+
592+
let syncData: ISyncData | null | undefined = undefined;
593+
594+
// Get Last Sync Data from Local
595+
let retrial = 1;
596+
while (syncData === undefined && retrial++ < 6 /* Retry 5 times */) {
597+
try {
598+
const content = (await this.fileService.readFile(this.lastSyncResource)).value.toString();
599+
try { syncData = content ? JSON.parse(content) : null; } catch (e) { /* Ignore */ }
600+
if (syncData && !isSyncData(syncData)) {
601+
this.logService.info(`${this.syncResourceLogLabel}: Last sync data stored locally is invalid.`);
602+
syncData = undefined;
603+
}
604+
break;
605+
} catch (error) {
606+
if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
607+
this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist locally.`);
608+
break;
609+
} else if (error instanceof UserDataSyncError) {
610+
throw error;
611+
} else {
612+
// log and retry
613+
this.logService.error(error, retrial);
614+
}
582615
}
616+
}
583617

584-
} catch (error) {
585-
if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
586-
this.logService.info(`${this.syncResourceLogLabel}: Not synced yet. Last sync resource does not exist.`);
587-
} else {
588-
// log error always except when file does not exist
589-
this.logService.error(error);
618+
// Get Last Sync Data from Remote
619+
if (syncData === undefined) {
620+
try {
621+
const content = await this.userDataSyncStoreService.resolveResourceContent(this.resource, lastSyncUserDataState.ref, this.collection, this.syncHeaders);
622+
syncData = content === null ? null : this.parseSyncData(content);
623+
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(syncData ? JSON.stringify(syncData) : ''));
624+
} catch (error) {
625+
if (error instanceof UserDataSyncError && error.code === UserDataSyncErrorCode.NotFound) {
626+
this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist on the server.`);
627+
} else {
628+
throw error;
629+
}
590630
}
591631
}
592-
return null;
632+
633+
// Last Sync Data Not Found
634+
if (syncData === undefined) {
635+
return null;
636+
}
637+
638+
return {
639+
...lastSyncUserDataState,
640+
syncData,
641+
} as T;
593642
}
594643

595644
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
596-
if (additionalProps['ref'] || additionalProps['content'] || additionalProps['version']) {
645+
if (additionalProps['ref'] || additionalProps['version']) {
597646
throw new Error('Cannot have core properties as additional');
598647
}
599648

600649
const version = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
601-
const lastSyncUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, version, ...additionalProps };
602-
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
650+
const lastSyncUserDataState: ILastSyncUserDataState = {
651+
ref: lastSyncRemoteUserData.ref,
652+
version,
653+
...additionalProps
654+
};
655+
656+
this.storageService.store(this.lastSyncUserDataStateKey, JSON.stringify(lastSyncUserDataState), StorageScope.APPLICATION, StorageTarget.MACHINE);
657+
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : ''));
658+
}
659+
660+
private async migrateLastSyncUserData(): Promise<string | undefined> {
661+
try {
662+
const content = await this.fileService.readFile(this.lastSyncResource);
663+
const userData = JSON.parse(content.value.toString());
664+
await this.fileService.del(this.lastSyncResource);
665+
if (userData.ref) {
666+
this.storageService.store(this.lastSyncUserDataStateKey, JSON.stringify({
667+
...userData,
668+
content: undefined,
669+
}), StorageScope.APPLICATION, StorageTarget.MACHINE);
670+
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(userData.content || ''));
671+
this.logService.info(`${this.syncResourceLogLabel}: Migrated data from last sync resource to last sync state.`);
672+
}
673+
} catch (error) {
674+
if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
675+
this.logService.debug(`${this.syncResourceLogLabel}: Migrating last sync user data. Resource does not exist.`);
676+
} else {
677+
this.logService.error(error);
678+
}
679+
}
680+
return this.storageService.get(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
603681
}
604682

605683
async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export function createSyncHeaders(executionId: string): IHeaders {
237237
export const enum UserDataSyncErrorCode {
238238
// Client Errors (>= 400 )
239239
Unauthorized = 'Unauthorized', /* 401 */
240+
NotFound = 'NotFound', /* 404 */
240241
Conflict = 'Conflict', /* 409 */
241242
Gone = 'Gone', /* 410 */
242243
PreconditionFailed = 'PreconditionFailed', /* 412 */
@@ -266,7 +267,6 @@ export const enum UserDataSyncErrorCode {
266267
LocalError = 'LocalError',
267268
IncompatibleLocalContent = 'IncompatibleLocalContent',
268269
IncompatibleRemoteContent = 'IncompatibleRemoteContent',
269-
UnresolvedConflicts = 'UnresolvedConflicts',
270270

271271
Unknown = 'Unknown',
272272
}
@@ -476,7 +476,7 @@ export interface IUserDataSyncTask {
476476
stop(): Promise<void>;
477477
}
478478

479-
export interface IUserDataManualSyncTask extends IDisposable {
479+
export interface IUserDataManualSyncTask {
480480
readonly id: string;
481481
merge(): Promise<void>;
482482
apply(): Promise<void>;

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

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
175175
that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
176176
that.updateLastSyncTime();
177177
},
178-
stop(): Promise<void> {
179-
cancellableToken.cancel();
180-
return that.stop();
181-
},
182-
dispose(): void {
178+
async stop(): Promise<void> {
183179
cancellableToken.cancel();
184-
that.stop();
180+
await that.stop();
181+
await that.resetLocal();
185182
}
186183
};
187184
}
@@ -388,6 +385,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
388385

389386
async resetLocal(): Promise<void> {
390387
this.checkEnablement();
388+
this._lastSyncTime = undefined;
391389
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION);
392390
for (const [synchronizer] of this.activeProfileSynchronizers.values()) {
393391
try {
@@ -600,21 +598,14 @@ class ProfileSynchronizer extends Disposable {
600598
this._enabled.push([synchronizer, order, disposables]);
601599
}
602600

603-
protected deRegisterSynchronizer(syncResource: SyncResource): void {
601+
private deRegisterSynchronizer(syncResource: SyncResource): void {
604602
const index = this._enabled.findIndex(([synchronizer]) => synchronizer.resource === syncResource);
605603
if (index !== -1) {
606-
const removed = this._enabled.splice(index, 1);
607-
for (const [synchronizer, , disposable] of removed) {
608-
if (synchronizer.status !== SyncStatus.Idle) {
609-
const hasConflicts = synchronizer.conflicts.conflicts.length > 0;
610-
synchronizer.stop();
611-
if (hasConflicts) {
612-
this.updateConflicts();
613-
}
614-
this.updateStatus();
615-
}
616-
disposable.dispose();
617-
}
604+
const [[synchronizer, , disposable]] = this._enabled.splice(index, 1);
605+
disposable.dispose();
606+
this.updateStatus();
607+
Promise.allSettled([synchronizer.stop(), synchronizer.resetLocal()])
608+
.then(null, error => this.logService.error(error));
618609
}
619610
}
620611

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

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { CancellationToken } from 'vs/base/common/cancellation';
77
import { Emitter, Event } from 'vs/base/common/event';
8-
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
8+
import { Disposable } from 'vs/base/common/lifecycle';
99
import { URI } from 'vs/base/common/uri';
1010
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
1111
import { ILogService } from 'vs/platform/log/common/log';
@@ -27,7 +27,7 @@ function reviewSyncResourceHandle(syncResourceHandle: ISyncResourceHandle): ISyn
2727

2828
export class UserDataSyncChannel implements IServerChannel {
2929

30-
private readonly manualSyncTasks = new Map<string, { manualSyncTask: IUserDataManualSyncTask; disposables: DisposableStore }>();
30+
private readonly manualSyncTasks = new Map<string, IUserDataManualSyncTask>();
3131
private readonly onManualSynchronizeResources = new Emitter<ManualSyncTaskEvent<[SyncResource, URI[]][]>>();
3232

3333
constructor(
@@ -95,37 +95,28 @@ export class UserDataSyncChannel implements IServerChannel {
9595

9696
switch (manualSyncTaskCommand) {
9797
case 'merge': return manualSyncTask.merge();
98-
case 'apply': return manualSyncTask.apply();
99-
case 'stop': return manualSyncTask.stop();
100-
case 'dispose': return this.disposeManualSyncTask(manualSyncTask);
98+
case 'apply': return manualSyncTask.apply().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
99+
case 'stop': return manualSyncTask.stop().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
101100
}
102101
}
103102

104103
throw new Error('Invalid call');
105104
}
106105

107106
private getManualSyncTask(manualSyncTaskId: string): IUserDataManualSyncTask {
108-
const value = this.manualSyncTasks.get(this.createKey(manualSyncTaskId));
109-
if (!value) {
107+
const manualSyncTask = this.manualSyncTasks.get(this.createKey(manualSyncTaskId));
108+
if (!manualSyncTask) {
110109
throw new Error(`Manual sync taks not found: ${manualSyncTaskId}`);
111110
}
112-
return value.manualSyncTask;
111+
return manualSyncTask;
113112
}
114113

115114
private async createManualSyncTask(): Promise<string> {
116-
const disposables = new DisposableStore();
117-
const manualSyncTask = disposables.add(await this.service.createManualSyncTask());
118-
this.manualSyncTasks.set(this.createKey(manualSyncTask.id), { manualSyncTask, disposables });
115+
const manualSyncTask = await this.service.createManualSyncTask();
116+
this.manualSyncTasks.set(this.createKey(manualSyncTask.id), manualSyncTask);
119117
return manualSyncTask.id;
120118
}
121119

122-
private disposeManualSyncTask(manualSyncTask: IUserDataManualSyncTask): void {
123-
manualSyncTask.dispose();
124-
const key = this.createKey(manualSyncTask.id);
125-
this.manualSyncTasks.get(key)?.disposables.dispose();
126-
this.manualSyncTasks.delete(key);
127-
}
128-
129120
private createKey(manualSyncTaskId: string): string { return `manualSyncTask-${manualSyncTaskId}`; }
130121

131122
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,10 @@ export class UserDataSyncStoreClient extends Disposable {
543543

544544
this._onTokenSucceed.fire();
545545

546+
if (context.res.statusCode === 404) {
547+
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because the requested resource is not found (404).`, url, UserDataSyncErrorCode.NotFound, context.res.statusCode, operationId);
548+
}
549+
546550
if (context.res.statusCode === 409) {
547551
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, url, UserDataSyncErrorCode.Conflict, context.res.statusCode, operationId);
548552
}

0 commit comments

Comments
 (0)