Repository: microsoft/vscode (TypeScript)
Date: 2022-10-26 15:36:49+00:00
Task type: Bug Fix

Issue:
User tasks keep syncing every 5min
Noticed this in Insiders. I'm not doing anything special:

![image](https://user-images.githubusercontent.com/5047891/193271555-65beec04-0908-4238-8391-ffb25319aa8c.png)

All these entries are empty:

<img width="816" alt="image" src="https://user-images.githubusercontent.com/5047891/193271651-718fe0cc-3b0e-44da-8b9c-87473346aac5.png">



Issue discussion:
Hmm interesting. I do not see this happening. Can you please let me know if you have `tasks.json` file exists in your user data directory (next to settings file) ?
Can you also please share the settings sync log on your system that syncs tasks like above?
@sandy081 I showed this to you once via screen sharing. The problem was resolved once I created a local `tasks.json` file with no contents. While that helped me avoid the endless syncing, I don't think the root problem is necessarily resolved.

Solution patch:
diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
--- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
+++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
@@ -733,6 +733,16 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
 		}
 	}
 
+	protected async deleteLocalFile(): Promise<void> {
+		try {
+			await this.fileService.del(this.file);
+		} catch (e) {
+			if (!(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) {
+				throw e;
+			}
+		}
+	}
+
 	private onFileChanges(e: FileChangesEvent): void {
 		if (!e.contains(this.file)) {
 			return;
diff --git a/src/vs/platform/userDataSync/common/tasksSync.ts b/src/vs/platform/userDataSync/common/tasksSync.ts
--- a/src/vs/platform/userDataSync/common/tasksSync.ts
+++ b/src/vs/platform/userDataSync/common/tasksSync.ts
@@ -18,7 +18,7 @@ import { AbstractFileSynchroniser, AbstractInitializer, IAcceptResult, IFileReso
 import { Change, IRemoteUserData, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
 
 interface ITasksSyncContent {
-	tasks: string;
+	tasks?: string;
 }
 
 interface ITasksResourcePreview extends IFileResourcePreview {
@@ -28,7 +28,7 @@ interface ITasksResourcePreview extends IFileResourcePreview {
 export function getTasksContentFromSyncContent(syncContent: string, logService: ILogService): string | null {
 	try {
 		const parsed = <ITasksSyncContent>JSON.parse(syncContent);
-		return parsed.tasks;
+		return parsed.tasks ?? null;
 	} catch (e) {
 		logService.error(e);
 		return null;
@@ -76,7 +76,7 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser
 		let hasRemoteChanged: boolean = false;
 		let hasConflicts: boolean = false;
 
-		if (remoteContent) {
+		if (remoteUserData.syncData) {
 			const localContent = fileContent ? fileContent.value.toString() : null;
 			if (!lastSyncContent // First time sync
 				|| lastSyncContent !== localContent // Local has forwarded
@@ -196,13 +196,17 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser
 			if (fileContent) {
 				await this.backupLocal(JSON.stringify(this.toTasksSyncContent(fileContent.value.toString())));
 			}
-			await this.updateLocalFileContent(content || '{}', fileContent, force);
+			if (content) {
+				await this.updateLocalFileContent(content, fileContent, force);
+			} else {
+				await this.deleteLocalFile();
+			}
 			this.logService.info(`${this.syncResourceLogLabel}: Updated local tasks`);
 		}
 
 		if (remoteChange !== Change.None) {
 			this.logService.trace(`${this.syncResourceLogLabel}: Updating remote tasks...`);
-			const remoteContents = JSON.stringify(this.toTasksSyncContent(content || '{}'));
+			const remoteContents = JSON.stringify(this.toTasksSyncContent(content));
 			remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
 			this.logService.info(`${this.syncResourceLogLabel}: Updated remote tasks`);
 		}
@@ -235,8 +239,8 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser
 		return null;
 	}
 
-	private toTasksSyncContent(tasks: string): ITasksSyncContent {
-		return { tasks };
+	private toTasksSyncContent(tasks: string | null): ITasksSyncContent {
+		return tasks ? { tasks } : {};
 	}
 
 }


Test patch:
diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts
--- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts
+++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts
@@ -444,6 +444,34 @@ suite('TasksSync', () => {
 		assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content);
 	});
 
+	test('when tasks file was removed in one client', async () => {
+		const fileService = client.instantiationService.get(IFileService);
+		const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource;
+		await fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({
+			'version': '2.0.0',
+			'tasks': []
+		})));
+		await testObject.sync(await client.getResourceManifest());
+
+		const client2 = disposableStore.add(new UserDataSyncClient(server));
+		await client2.setUp(true);
+		await client2.sync();
+
+		const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource;
+		const fileService2 = client2.instantiationService.get(IFileService);
+		fileService2.del(tasksResource2);
+		await client2.sync();
+
+		await testObject.sync(await client.getResourceManifest());
+
+		assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
+		const lastSyncUserData = await testObject.getLastSyncUserData();
+		const remoteUserData = await testObject.getRemoteUserData(null);
+		assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content!, client.instantiationService.get(ILogService)), null);
+		assert.strictEqual(getTasksContentFromSyncContent(remoteUserData!.syncData!.content!, client.instantiationService.get(ILogService)), null);
+		assert.strictEqual(await fileService.exists(tasksResource), false);
+	});
+
 	test('when tasks file is created after first sync', async () => {
 		const fileService = client.instantiationService.get(IFileService);
 		const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource;
@@ -491,7 +519,7 @@ suite('TasksSync', () => {
 		assert.deepStrictEqual(server.requests, []);
 	});
 
-	test('sync profile snippets', async () => {
+	test('sync profile tasks', async () => {
 		const client2 = disposableStore.add(new UserDataSyncClient(server));
 		await client2.setUp(true);
 		const profile = await client2.instantiationService.get(IUserDataProfilesService).createNamedProfile('profile1');