Skip to content

Commit

Permalink
Handle mail export throttling
Browse files Browse the repository at this point in the history
issue #8109

Co-authored-by: paw <paw-hub@users.noreply.github.com>
  • Loading branch information
BijinDev and paw-hub committed Dec 17, 2024
1 parent 43d9d64 commit 9a68975
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 10 deletions.
11 changes: 10 additions & 1 deletion src/common/api/common/error/SuspensionError.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
//@bundleInto:common-min

import { TutanotaError } from "@tutao/tutanota-error"
import { filterInt } from "@tutao/tutanota-utils"

export class SuspensionError extends TutanotaError {
constructor(message: string) {
// milliseconds to wait
readonly data: string | null
constructor(message: string, suspensionTime: string | null) {
super("SuspensionError", message)

if (suspensionTime != null && Number.isNaN(filterInt(suspensionTime))) {
throw new Error("invalid suspension time value (NaN)")
}

this.data = suspensionTime
}
}
Empty file.
7 changes: 6 additions & 1 deletion src/common/api/worker/rest/RestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ export class RestClient {
const suspensionTime = xhr.getResponseHeader("Retry-After") || xhr.getResponseHeader("Suspension-Time")

if (isSuspensionResponse(xhr.status, suspensionTime) && options.suspensionBehavior === SuspensionBehavior.Throw) {
reject(new SuspensionError(`blocked for ${suspensionTime}, not suspending`))
reject(
new SuspensionError(
`blocked for ${suspensionTime}, not suspending (${xhr.status})`,
suspensionTime && (parseInt(suspensionTime) * 1000).toString(),
),
)
} else if (isSuspensionResponse(xhr.status, suspensionTime)) {
this.suspensionHandler.activateSuspensionIfInactive(Number(suspensionTime), resourceURL)

Expand Down
26 changes: 19 additions & 7 deletions src/mail-app/native/main/MailExportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ import Stream from "mithril/stream"
import stream from "mithril/stream"
import { MailBag } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { GENERATED_MAX_ID, getElementId, isSameId } from "../../../common/api/common/utils/EntityUtils.js"
import { assertNotNull, delay, isNotNull, lastThrow, ofClass, promiseMap } from "@tutao/tutanota-utils"
import {assertNotNull, delay, filterInt, isNotNull, lastThrow, ofClass, promiseMap} from "@tutao/tutanota-utils"
import { HtmlSanitizer } from "../../../common/misc/HtmlSanitizer.js"
import { ExportFacade } from "../../../common/native/common/generatedipc/ExportFacade.js"
import { LoginController } from "../../../common/api/main/LoginController.js"
import { FileController } from "../../../common/file/FileController.js"
import { CancelledError } from "../../../common/api/common/error/CancelledError.js"
import { BulkMailLoader } from "../../workerUtils/index/BulkMailLoader.js"
import { FileOpenError } from "../../../common/api/common/error/FileOpenError.js"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
import { NotFoundError } from "../../../common/api/common/error/RestError.js"
import type { TranslationText } from "../../../common/misc/LanguageViewModel"
import {SuspensionError} from "../../../common/api/common/error/SuspensionError";
import {FileController} from "../../../common/file/FileController";
import {BulkMailLoader} from "../../workerUtils/index/BulkMailLoader";

export type MailExportState =
| { type: "idle" }
| { type: "exporting"; mailboxDetail: MailboxDetail; progress: number; exportedMails: number }
| { type: "error"; message: string }
| { type: "error"; message: TranslationText }
| {
type: "finished"
mailboxDetail: MailboxDetail
Expand Down Expand Up @@ -89,6 +91,7 @@ export class MailExportController {
return
}
this._state({ type: "exporting", mailboxDetail: mailboxDetail, progress: 0, exportedMails: exportState.exportedMails })
this._lastExport = new Date()
await this.resumeExport(mailboxDetail, exportState.mailBagId, exportState.mailId)
} else if (exportState.type === "finished") {
const mailboxDetail = await this.mailboxModel.getMailboxDetailByMailboxId(exportState.mailboxId)
Expand Down Expand Up @@ -126,9 +129,10 @@ export class MailExportController {
}

private async runExport(mailboxDetail: MailboxDetail, mailBags: MailBag[], mailId: Id) {
const startTime = assertNotNull(this._lastExport)
for (const mailBag of mailBags) {
await this.exportMailBag(mailBag, mailId)
if (this._state().type !== "exporting") {
if (this._state().type !== "exporting" || this._lastExport !== startTime) {
return
}
}
Expand Down Expand Up @@ -178,7 +182,7 @@ export class MailExportController {
await this.exportFacade.saveMailboxExport(mailBundle, this.userId, mailBag._id, getElementId(mail))
} catch (e) {
if (e instanceof FileOpenError) {
this._state({ type: "error", message: e.message })
this._state({ type: "error", message: () => e.message })
return
} else {
throw e
Expand All @@ -195,10 +199,18 @@ export class MailExportController {
if (isOfflineError(e)) {
console.log(TAG, "Offline, will retry later")
await delay(1000 * 60) // 1 min
console.log(TAG, "Trying to continue with export")
} else if (e instanceof SuspensionError) {
const timeToWait = Math.max(filterInt(assertNotNull(e.data)), 1)
console.log(TAG, `Pausing for ${Math.floor(timeToWait / 1000 + 0.5)} seconds`)
const currentExportTime = this._lastExport
await delay(timeToWait)
if (this._state().type !== "exporting" || this._lastExport !== currentExportTime) {
return
}
} else {
throw e
}
console.log(TAG, "Trying to continue with export")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/mail-app/settings/MailExportSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class MailExportSettings implements Component<MailExportSettingsAttrs> {
case "error":
return [
m(".flex-space-between.items-center.mt.mb-s", [
m("small.noselect", state.message),
m("small.noselect", lang.getMaybeLazy(state.message)),
m(Button, {
label: "retry_action",
click: () => {
Expand Down
18 changes: 18 additions & 0 deletions test/tests/native/main/MailExportControllerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createDataFile } from "../../../../src/common/api/common/DataFile.js"
import { makeMailBundle } from "../../../../src/mail-app/mail/export/Bundler.js"
import { MailboxExportState } from "../../../../src/common/desktop/export/MailboxExportPersistence.js"
import { BulkMailLoader } from "../../../../src/mail-app/workerUtils/index/BulkMailLoader.js"
import { SuspensionError } from "../../../../src/common/api/common/error/SuspensionError"

o.spec("MailExportController", function () {
const userId = "userId"
Expand Down Expand Up @@ -184,4 +185,21 @@ o.spec("MailExportController", function () {
verify(exportFacade.endMailboxExport(userId))
})
})

o.spec("handle errors", function () {
o.test("SuspensionError", async () => {
let wasThrown = false
when(loader.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything())).thenDo(() => {
if (wasThrown) {
return Promise.resolve([])
} else {
wasThrown = true
return Promise.reject(new SuspensionError(":(", "10"))
}
})
await controller.startExport(mailboxDetail)
verify(loader.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything()), { times: 3 + 1 })
o(wasThrown).equals(true)
})
})
})

0 comments on commit 9a68975

Please sign in to comment.