Repository navigation
[PB-4449]: refactor/upload retry of what failed in an upload #2266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
654605e
4e097a9
a7a45d5
e744b38
9682335
15eb1a4
9e45bde
5bf545f
baf222d
0ee4887
cd0c921
e6c2586
a563214
efddfe8
f37e4fb
2d07964
d990886
b14759d
5ccb6e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,12 @@ import { | |
| import { PersistUploadRepository } from 'app/repositories/DatabaseUploadRepository'; | ||
| import { IRoot } from 'app/store/slices/storage/types'; | ||
| import tasksService from 'app/tasks/services/tasks.service'; | ||
| import { TaskStatus, TaskType, UploadFileTask, UploadFolderTask } from 'app/tasks/types'; | ||
| import { FailedUploadItem, TaskStatus, TaskType, UploadFileTask, UploadFolderTask } from 'app/tasks/types'; | ||
| import { errorMerge, folderRemovalFailedMerge } from 'app/tasks/upload/uploadTaskErrors'; | ||
| import errorService from 'services/error.service'; | ||
| import referralService from 'services/referral.service'; | ||
| import { uniqueId } from 'utils/lodash'; | ||
| import { toFailedUploadItem } from './failedUploadItem'; | ||
|
|
||
| interface UploadProps { | ||
| destinationFolderUuid: string; | ||
|
|
@@ -45,7 +47,7 @@ interface RootFolderUpload { | |
| totalItems: number; | ||
| uploadedItems: number; | ||
| isCancelled: boolean; | ||
| hasFailedItems: boolean; | ||
| isUploadTracked: boolean; | ||
| createdFolder?: DriveFolderData; | ||
| } | ||
|
|
||
|
|
@@ -66,6 +68,7 @@ export class Upload { | |
| private readonly taskIdByItem = new Map<UploadItem, string>(); | ||
| private readonly rootFolders = new Map<UploadItem, RootFolderUpload>(); | ||
| private readonly cancelledRootFolderDeletions: Promise<void>[] = []; | ||
| private readonly failedItems = new Map<UploadItem, FailedUploadItem>(); | ||
|
|
||
| constructor(props: UploadProps) { | ||
| this.props = props; | ||
|
|
@@ -114,6 +117,7 @@ export class Upload { | |
| showNotification: true, | ||
| cancellable: true, | ||
| stop: () => Promise.resolve(this.cancelRootFolder(item)), | ||
| retry: (failedItem) => this.retry(item, failedItem), | ||
| }); | ||
|
|
||
| this.taskIdByItem.set(item, taskId); | ||
|
|
@@ -122,7 +126,7 @@ export class Upload { | |
| totalItems: countItemsIn(item.folder), | ||
| uploadedItems: 0, | ||
| isCancelled: false, | ||
| hasFailedItems: false, | ||
| isUploadTracked: false, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -143,6 +147,7 @@ export class Upload { | |
| currentFolderId: destinationFolderUuid, | ||
| }, | ||
| stop: () => Promise.resolve(this.manager.cancelItem(item)), | ||
| retry: (failedItem) => this.retry(item, failedItem), | ||
| }); | ||
|
|
||
| this.taskIdByItem.set(item, taskId); | ||
|
|
@@ -180,6 +185,7 @@ export class Upload { | |
| else this.countUploadedInRootFolder(item, uploadedItem); | ||
|
|
||
| const isFile = item.kind === UploadItemKind.File; | ||
| this.clearFailure(item); | ||
| this.props.onItemUploaded?.(uploadedItem, getParentFolderUuid(item), !isFile); | ||
|
|
||
| if (isFile) { | ||
|
|
@@ -217,8 +223,7 @@ export class Upload { | |
| } | ||
|
|
||
| private onItemFailed(item: UploadItem, failure: UploadFailure): void { | ||
| const rootFolder = this.rootFolders.get(getTopLevelItem(item)); | ||
| if (rootFolder) rootFolder.hasFailedItems = true; | ||
| this.recordFailure(item, failure); | ||
|
|
||
| const taskId = this.getTaskIdOf(item); | ||
| if (!taskId) return; | ||
|
|
@@ -271,10 +276,55 @@ export class Upload { | |
| merge: { status: TaskStatus.Success, itemUUID: { rootFolderUUID: createdFolder.uuid } }, | ||
| }); | ||
|
|
||
| const hasEverythingUploaded = !rootFolder.hasFailedItems; | ||
| if (hasEverythingUploaded) { | ||
| const hasEverythingUploaded = this.getFailuresOf(item).length === 0; | ||
| const shouldTrackUpload = hasEverythingUploaded && !rootFolder.isUploadTracked; | ||
| if (shouldTrackUpload) { | ||
| rootFolder.isUploadTracked = true; | ||
| referralService.trackFolderUpload(); | ||
| logNetworkInfoForUpload({ folderName: item.folder.name }); | ||
| } | ||
| } | ||
|
|
||
| private async retry(topLevelItem: UploadItem, pickedFailedItem?: FailedUploadItem): Promise<void> { | ||
| const retryableFailures = this.getFailuresOf(topLevelItem).filter(([, failedItem]) => failedItem.isRetryable); | ||
| const failuresToRetry = pickedFailedItem | ||
| ? retryableFailures.filter(([, failedItem]) => failedItem.id === pickedFailedItem.id) | ||
| : retryableFailures; | ||
|
|
||
| const hasNothingToRetry = failuresToRetry.length === 0; | ||
| if (hasNothingToRetry) return; | ||
|
|
||
| for (const [item, failedItem] of failuresToRetry) this.failedItems.set(item, { ...failedItem, isRetrying: true }); | ||
| this.syncFailedItemsToTask(topLevelItem); | ||
|
|
||
| const taskId = this.getTaskIdOf(topLevelItem) as string; | ||
| const rootFolder = this.rootFolders.get(topLevelItem); | ||
| const progress = rootFolder ? tasksService.findTask(taskId)?.progress : 0; | ||
| tasksService.updateTask({ taskId, merge: { status: TaskStatus.InProcess, subtitle: undefined, progress } }); | ||
|
|
||
| const itemsToRetry = failuresToRetry.map(([item]) => item); | ||
| await this.manager.run(itemsToRetry); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since each retry has its own manager, it only waits for its own items. |
||
|
|
||
| if (rootFolder) this.finishRootFolder(topLevelItem as UploadFolderItem, rootFolder); | ||
| } | ||
|
|
||
| private getFailuresOf(topLevelItem: UploadItem): [UploadItem, FailedUploadItem][] { | ||
| return [...this.failedItems].filter(([item]) => getTopLevelItem(item) === topLevelItem); | ||
| } | ||
|
|
||
| private recordFailure(item: UploadItem, failure: UploadFailure): void { | ||
| const failureId = this.failedItems.get(item)?.id ?? uniqueId(); | ||
| this.failedItems.set(item, toFailedUploadItem(failureId, item, failure)); | ||
| this.syncFailedItemsToTask(getTopLevelItem(item)); | ||
| } | ||
|
|
||
| private clearFailure(item: UploadItem): void { | ||
| const hadFailed = this.failedItems.delete(item); | ||
| if (hadFailed) this.syncFailedItemsToTask(getTopLevelItem(item)); | ||
| } | ||
|
|
||
| private syncFailedItemsToTask(topLevelItem: UploadItem): void { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every failure and every success calls
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failures are now grouped by top-level item: with storage full in one folder, 10,000 files went from 2.5 s to 0.57 s. We kept the live sync so the list updates during the upload.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Live updates make sense. Throttling the sync (e.g. every 200 ms) keeps them live and avoids 3003 updates for 3000 failures in one folder. |
||
| const failedItems = this.getFailuresOf(topLevelItem).map(([, failedItem]) => failedItem); | ||
| tasksService.updateTask({ taskId: this.getTaskIdOf(topLevelItem) as string, merge: { failedItems } }); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.