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 11, 2024
1 parent 7c6f9cb commit 9c206df
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 14 deletions.
3 changes: 2 additions & 1 deletion src/common/api/worker/facades/lazy/MailExportTokenFacade.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AccessExpiredError } from "../../../common/error/RestError.js"
import { MailExportTokenService } from "../../../entities/tutanota/Services.js"
import { IServiceExecutor } from "../../../common/ServiceRequest.js"
import { SuspensionBehavior } from "../../rest/RestClient"

const TAG = "[MailExportTokenFacade]"

Expand Down Expand Up @@ -65,7 +66,7 @@ export class MailExportTokenFacade {
}

this.currentExportToken = null
this.currentExportTokenRequest = this.serviceExecutor.post(MailExportTokenService, null).then(
this.currentExportTokenRequest = this.serviceExecutor.post(MailExportTokenService, null, { suspensionBehavior: SuspensionBehavior.Suspend }).then(
(result) => {
this.currentExportToken = result.mailExportToken as MailExportToken
this.currentExportTokenRequest = null
Expand Down
2 changes: 2 additions & 0 deletions src/common/misc/TranslationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1817,3 +1817,5 @@ export type TranslationKeyType =
| "you_label"
| "emptyString_msg"
| "exportFinished_label"
| "exportErrorTooManyRequests_label"
| "exportErrorServiceUnavailable_label"
26 changes: 18 additions & 8 deletions src/mail-app/native/main/MailExportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@ 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, isNotNull, lastThrow } 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 { ServiceUnavailableError, TooManyRequestsError } from "../../../common/api/common/error/RestError.js"
import { MailExportFacade } from "../../../common/api/worker/facades/lazy/MailExportFacade.js"
import type { TranslationText } from "../../../common/misc/LanguageViewModel"

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 @@ -127,8 +126,19 @@ export class MailExportController {

private async runExport(mailboxDetail: MailboxDetail, mailBags: MailBag[], mailId: Id) {
for (const mailBag of mailBags) {
await this.exportMailBag(mailBag, mailId)
if (this._state().type !== "exporting") {
try {
await this.exportMailBag(mailBag, mailId)
if (this._state().type !== "exporting") {
return
}
} catch (e) {
if (e instanceof TooManyRequestsError) {
this._state({ type: "error", message: "exportErrorTooManyRequests_label" })
} else if (e instanceof ServiceUnavailableError) {
this._state({ type: "error", message: "exportErrorServiceUnavailable_label" })
} else {
throw e
}
return
}
}
Expand Down Expand Up @@ -171,7 +181,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 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
2 changes: 2 additions & 0 deletions src/mail-app/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,5 +1837,7 @@ export default {
"yourMessage_label": "Deine Nachricht",
"you_label": "Du",
"exportFinished_label": "Export finished",
"exportErrorTooManyRequests_label": "Too many exports were requested recently",
"exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later"
}
}
2 changes: 2 additions & 0 deletions src/mail-app/translations/de_sie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,5 +1837,7 @@ export default {
"yourMessage_label": "Ihre Nachricht",
"you_label": "Sie",
"exportFinished_label": "Export finished",
"exportErrorTooManyRequests_label": "Too many exports were requested recently",
"exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later"
}
}
2 changes: 2 additions & 0 deletions src/mail-app/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1833,5 +1833,7 @@ export default {
"yourMessage_label": "Your message",
"you_label": "You",
"exportFinished_label": "Export finished",
"exportErrorTooManyRequests_label": "Too many exports were requested recently",
"exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later"
}
}
12 changes: 8 additions & 4 deletions test/tests/api/worker/facades/MailExportTokenFacadeTest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import o from "@tutao/otest"
import { func, object, when } from "testdouble"
import { func, matchers, object, when } from "testdouble"
import { createMailExportTokenServicePostOut } from "../../../../../src/common/api/entities/tutanota/TypeRefs"
import { MailExportTokenService } from "../../../../../src/common/api/entities/tutanota/Services"
import { AccessExpiredError, TooManyRequestsError } from "../../../../../src/common/api/common/error/RestError"
Expand All @@ -23,7 +23,9 @@ o.spec("MailExportTokenFacade", () => {
const expected = "result"
const cb = func<(token: string) => Promise<string>>()
when(cb(validToken)).thenResolve(expected)
when(serviceExecutor.post(MailExportTokenService, null)).thenResolve(createMailExportTokenServicePostOut({ mailExportToken: validToken }))
when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenResolve(
createMailExportTokenServicePostOut({ mailExportToken: validToken }),
)

const result = await facade.loadWithToken(cb)

Expand All @@ -47,7 +49,9 @@ o.spec("MailExportTokenFacade", () => {
when(cb(validToken)).thenResolve(expected)
when(cb(expiredToken)).thenReject(new AccessExpiredError("token expired"))
facade._setCurrentExportToken(expiredToken)
when(serviceExecutor.post(MailExportTokenService, null)).thenResolve(createMailExportTokenServicePostOut({ mailExportToken: validToken }))
when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenResolve(
createMailExportTokenServicePostOut({ mailExportToken: validToken }),
)

const result = await facade.loadWithToken(cb)

Expand All @@ -57,7 +61,7 @@ o.spec("MailExportTokenFacade", () => {
o.test("when requesting token fails none are stored", async () => {
const cb = func<(token: string) => Promise<string>>()
when(cb(expiredToken)).thenReject(new AccessExpiredError("token expired"))
when(serviceExecutor.post(MailExportTokenService, null)).thenReject(new TooManyRequestsError("no more tokens :("))
when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenReject(new TooManyRequestsError("no more tokens :("))

await o(() => facade.loadWithToken(cb)).asyncThrows(TooManyRequestsError)

Expand Down
19 changes: 19 additions & 0 deletions test/tests/native/main/MailExportControllerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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 { MailExportFacade } from "../../../../src/common/api/worker/facades/lazy/MailExportFacade.js"
import { ServiceUnavailableError, TooManyRequestsError } from "../../../../src/common/api/common/error/RestError"
import type { TranslationText } from "../../../../src/common/misc/LanguageViewModel"

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

o.spec("handle errors", function () {
o.test("TooManyRequestsError", async () => {
when(mailExportFacade.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything())).thenReject(new TooManyRequestsError(":("))
await controller.startExport(mailboxDetail)
const currentState = controller.state() as { type: string; message: TranslationText }
o(currentState.type).equals("error")
o(currentState.message).equals("exportErrorTooManyRequests_label")
})
o.test("ServiceUnavailableError", async () => {
when(mailExportFacade.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything())).thenReject(new ServiceUnavailableError(":("))
await controller.startExport(mailboxDetail)
const currentState = controller.state() as { type: string; message: TranslationText }
o(currentState.type).equals("error")
o(currentState.message).equals("exportErrorServiceUnavailable_label")
})
})
})

0 comments on commit 9c206df

Please sign in to comment.