@remit/imap-worker 0.0.86 → 0.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +10 -2
- package/src/account-check.ts +1 -1
- package/src/body-sync-gate.test.ts +1 -1
- package/src/body-sync-gate.ts +1 -1
- package/src/e2e-processor-shim.ts +6 -21
- package/src/emit.ts +1 -1
- package/src/failure-kind.ts +1 -1
- package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
- package/src/handlers/append-sent-message.ts +3 -24
- package/src/handlers/delete-account-objects.test.ts +1 -1
- package/src/handlers/delete-account-objects.ts +1 -1
- package/src/handlers/empty-trash.sqlite.test.ts +209 -0
- package/src/handlers/empty-trash.ts +3 -24
- package/src/handlers/flag-push.ts +11 -18
- package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
- package/src/handlers/mailbox-management.ts +12 -48
- package/src/handlers/message-copy.sqlite.test.ts +298 -0
- package/src/handlers/message-copy.ts +31 -49
- package/src/handlers/message-delete-terminal.ts +12 -10
- package/src/handlers/message-delete.sqlite.test.ts +291 -0
- package/src/handlers/message-delete.ts +26 -32
- package/src/handlers/message-move-terminal.ts +12 -10
- package/src/handlers/message-move.sqlite.test.ts +271 -0
- package/src/handlers/message-move.test.ts +76 -863
- package/src/handlers/message-move.ts +49 -39
- package/src/handlers/placement-move-push.test.ts +96 -329
- package/src/handlers/placement-move-push.ts +75 -29
- package/src/handlers/placement-push.sqlite.test.ts +298 -0
- package/src/handlers/probe-message-id.sqlite.test.ts +197 -0
- package/src/handlers/probe-message-id.ts +15 -0
- package/src/handlers/sync-mailboxes.test.ts +0 -1
- package/src/handlers/sync-mailboxes.ts +1 -1
- package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
- package/src/handlers/sync-message-body.test.ts +3 -299
- package/src/handlers/sync-message-body.ts +21 -11
- package/src/handlers/sync-messages.sqlite.test.ts +183 -0
- package/src/handlers/sync-messages.ts +27 -33
- package/src/index.ts +5 -3
- package/src/mail-sync-selection.sqlite.test.ts +16 -18
- package/src/poller.ts +5 -4
- package/src/processor.test.ts +1 -1
- package/src/processor.ts +1 -1
- package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
- package/src/scheduler/attachment-sweep.ts +1 -1
- package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
- package/src/scheduler/calendar-subscriptions.ts +6 -6
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
- package/src/scheduler/handler.ts +12 -1
- package/src/scheduler/loop.test.ts +2 -0
- package/src/scheduler/run-tick.sqlite.test.ts +192 -0
- package/src/scheduler/run-tick.ts +39 -1
- package/src/scheduler/runner.ts +21 -1
- package/src/scheduler/stranded-placement-redrive.ts +265 -0
- package/src/stranded-placement.sqlite.test.ts +795 -0
- package/src/test-helpers/test-world.ts +398 -0
- package/src/thread-join.sqlite.test.ts +115 -0
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +138 -14
- package/src/with-oauth-lifecycle.ts +1 -1
- package/src/handlers/append-sent-message.test.ts +0 -529
- package/src/handlers/empty-trash.test.ts +0 -728
- package/src/handlers/flag-push-placement-race.test.ts +0 -385
- package/src/handlers/flag-push.test.ts +0 -325
- package/src/handlers/mailbox-management.test.ts +0 -850
- package/src/handlers/message-copy.test.ts +0 -654
- package/src/handlers/message-delete.test.ts +0 -1869
- package/src/handlers/message-move-terminal.test.ts +0 -412
- package/src/handlers/sync-messages.test.ts +0 -569
- package/src/scheduler/attachment-sweep.test.ts +0 -169
- package/src/scheduler/run-tick.test.ts +0 -478
- package/src/with-oauth-lifecycle.test.ts +0 -318
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
triggerAccountSync,
|
|
5
5
|
} from "@remit/backend/trigger-sync";
|
|
6
6
|
import type { AccountItem, IAccountRepository } from "@remit/data-ports";
|
|
7
|
-
import type { Logger } from "@remit/logger
|
|
7
|
+
import type { Logger } from "@remit/logger";
|
|
8
8
|
import { hasStoredCredential } from "@remit/mailbox-service/account-credentials";
|
|
9
9
|
import pMap from "p-map";
|
|
10
10
|
import {
|
|
@@ -53,6 +53,10 @@ export interface RunSchedulerTickDeps {
|
|
|
53
53
|
now: number,
|
|
54
54
|
intervalMs: number,
|
|
55
55
|
) => Promise<CalendarSubscriptionTally>;
|
|
56
|
+
redriveStrandedPlacements: (
|
|
57
|
+
account: AccountItem,
|
|
58
|
+
now: number,
|
|
59
|
+
) => Promise<number>;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export interface SchedulerTickResult {
|
|
@@ -63,6 +67,8 @@ export interface SchedulerTickResult {
|
|
|
63
67
|
sweepFailed: number;
|
|
64
68
|
subscriptionsRefreshed: number;
|
|
65
69
|
subscriptionsFailed: number;
|
|
70
|
+
redriven: number;
|
|
71
|
+
redriveFailed: number;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -135,6 +141,8 @@ export const runSchedulerTick = async (
|
|
|
135
141
|
let skipped = 0;
|
|
136
142
|
let swept = 0;
|
|
137
143
|
let sweepFailed = 0;
|
|
144
|
+
let redriven = 0;
|
|
145
|
+
let redriveFailed = 0;
|
|
138
146
|
|
|
139
147
|
do {
|
|
140
148
|
const page = await accountService.listAllAccountsPage({
|
|
@@ -202,6 +210,32 @@ export const runSchedulerTick = async (
|
|
|
202
210
|
sweepFailed += outcomes.filter((ok) => !ok).length;
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
// Contained like the sweep, and for the same reason: a failed re-drive
|
|
214
|
+
// costs one account this tick, never the scheduler. Eligible accounts
|
|
215
|
+
// only, since a re-driven event needs a connection to settle anything.
|
|
216
|
+
const redrives = await pMap(
|
|
217
|
+
page.items.filter(isEligible),
|
|
218
|
+
(account) =>
|
|
219
|
+
deps.redriveStrandedPlacements(account, now).then(
|
|
220
|
+
(count) => count,
|
|
221
|
+
(error: unknown) => {
|
|
222
|
+
log.error(
|
|
223
|
+
{ accountId: account.accountId, error },
|
|
224
|
+
"Stranded placement re-drive failed for account",
|
|
225
|
+
);
|
|
226
|
+
return null;
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
{ concurrency: SCHEDULER_ENQUEUE_CONCURRENCY },
|
|
230
|
+
);
|
|
231
|
+
for (const count of redrives) {
|
|
232
|
+
if (count === null) {
|
|
233
|
+
redriveFailed += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
redriven += count;
|
|
237
|
+
}
|
|
238
|
+
|
|
205
239
|
cursor = page.cursor ?? undefined;
|
|
206
240
|
} while (cursor);
|
|
207
241
|
|
|
@@ -231,6 +265,8 @@ export const runSchedulerTick = async (
|
|
|
231
265
|
sweepFailed,
|
|
232
266
|
subscriptionsRefreshed,
|
|
233
267
|
subscriptionsFailed,
|
|
268
|
+
redriven,
|
|
269
|
+
redriveFailed,
|
|
234
270
|
},
|
|
235
271
|
"Scheduled-sync tick complete",
|
|
236
272
|
);
|
|
@@ -243,5 +279,7 @@ export const runSchedulerTick = async (
|
|
|
243
279
|
sweepFailed,
|
|
244
280
|
subscriptionsRefreshed,
|
|
245
281
|
subscriptionsFailed,
|
|
282
|
+
redriven,
|
|
283
|
+
redriveFailed,
|
|
246
284
|
};
|
|
247
285
|
};
|
package/src/scheduler/runner.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
|
|
3
4
|
import { getClient } from "@remit/backend/client";
|
|
4
|
-
import { createLogger } from "@remit/logger
|
|
5
|
+
import { createLogger } from "@remit/logger";
|
|
6
|
+
import { installNodeLogger } from "@remit/logger-lambda/node";
|
|
5
7
|
import { clearHeartbeats, createHeartbeat } from "@remit/sqs-client/heartbeat";
|
|
6
8
|
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
7
9
|
|
|
8
10
|
import { env } from "expect-env";
|
|
11
|
+
import { emitEvent } from "../emit.js";
|
|
9
12
|
import { buildAttachmentSweep } from "./attachment-sweep.js";
|
|
10
13
|
import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
|
|
11
14
|
import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
|
|
12
15
|
import { runSchedulerLoop } from "./loop.js";
|
|
13
16
|
import { runSchedulerTick } from "./run-tick.js";
|
|
17
|
+
import { redriveStrandedPlacements } from "./stranded-placement-redrive.js";
|
|
18
|
+
|
|
19
|
+
installNodeLogger();
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* The scheduled-sync runner for every deployment that has no EventBridge: the
|
|
@@ -36,6 +42,7 @@ const log = createLogger();
|
|
|
36
42
|
|
|
37
43
|
const mailboxesQueueUrl = env.SQS_QUEUE_URL_MAILBOXES;
|
|
38
44
|
const sqsClient = createQueueProducer({ queueUrl: mailboxesQueueUrl });
|
|
45
|
+
const accountFanoutQueueUrl = env.SQS_QUEUE_URL_ACCOUNT_FANOUT;
|
|
39
46
|
|
|
40
47
|
const tickIntervalMs = getTickIntervalMs();
|
|
41
48
|
const offlineIntervalMs = getOfflineIntervalMs();
|
|
@@ -71,6 +78,13 @@ const runLoop = async (): Promise<void> => {
|
|
|
71
78
|
await clearHeartbeats().catch(onClearError);
|
|
72
79
|
const client = await getClient();
|
|
73
80
|
const { account } = client;
|
|
81
|
+
await finishHalfDeletedAccounts({
|
|
82
|
+
account,
|
|
83
|
+
configImport: client.configImport,
|
|
84
|
+
sqsClient: createQueueProducer({ queueUrl: accountFanoutQueueUrl }),
|
|
85
|
+
accountFanoutQueueUrl,
|
|
86
|
+
log,
|
|
87
|
+
});
|
|
74
88
|
await runSchedulerLoop({
|
|
75
89
|
tick: runSchedulerTick,
|
|
76
90
|
tickDeps: {
|
|
@@ -85,6 +99,12 @@ const runLoop = async (): Promise<void> => {
|
|
|
85
99
|
client,
|
|
86
100
|
log,
|
|
87
101
|
),
|
|
102
|
+
redriveStrandedPlacements: (stranded, now) =>
|
|
103
|
+
redriveStrandedPlacements(
|
|
104
|
+
{ client, emit: emitEvent, log },
|
|
105
|
+
stranded,
|
|
106
|
+
now,
|
|
107
|
+
),
|
|
88
108
|
},
|
|
89
109
|
heartbeat: createHeartbeat("tick"),
|
|
90
110
|
onHeartbeatError: onBeatError,
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { RemitClient } from "@remit/backend/client";
|
|
2
|
+
import type { AccountItem, MessageItem } from "@remit/data-ports";
|
|
3
|
+
import { trashMailboxAt } from "@remit/data-ports/folder-role";
|
|
4
|
+
import {
|
|
5
|
+
deriveCopyMessageId,
|
|
6
|
+
deriveMessageId,
|
|
7
|
+
isValidMessageId,
|
|
8
|
+
} from "@remit/data-ports/id";
|
|
9
|
+
import { PATHLESS_MUTATION_EVENT_SCHEMA_VERSION } from "@remit/data-ports/mutation-events";
|
|
10
|
+
import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
|
|
11
|
+
import type { Logger } from "@remit/logger";
|
|
12
|
+
import { isCursorRebuildNeeded } from "@remit/mailbox-service";
|
|
13
|
+
import type {
|
|
14
|
+
MessageCopyEventV3,
|
|
15
|
+
MessageDeleteEventV3,
|
|
16
|
+
MessageMoveEventV3,
|
|
17
|
+
PlacementMovePushEvent,
|
|
18
|
+
} from "../events.js";
|
|
19
|
+
|
|
20
|
+
/** Well past three attempts on a 60 s visibility timeout. */
|
|
21
|
+
export const STRANDED_PLACEMENT_AFTER_MILLIS = 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
type Unstamped<T> = Omit<T, "eventId" | "timestamp">;
|
|
24
|
+
|
|
25
|
+
export type StrandedPlacementEvent =
|
|
26
|
+
| Unstamped<PlacementMovePushEvent>
|
|
27
|
+
| Unstamped<MessageDeleteEventV3>
|
|
28
|
+
| Unstamped<MessageMoveEventV3>
|
|
29
|
+
| Unstamped<MessageCopyEventV3>;
|
|
30
|
+
|
|
31
|
+
export interface StrandedPlacementRedriveDeps {
|
|
32
|
+
client: Pick<
|
|
33
|
+
RemitClient,
|
|
34
|
+
| "mailbox"
|
|
35
|
+
| "mailboxSpecialUse"
|
|
36
|
+
| "message"
|
|
37
|
+
| "placementMove"
|
|
38
|
+
| "threadMessage"
|
|
39
|
+
>;
|
|
40
|
+
emit: (event: StrandedPlacementEvent) => Promise<void>;
|
|
41
|
+
log: Logger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isStrandedCopy = (row: MessageItem): boolean =>
|
|
45
|
+
row.status === MessageStatus.moving &&
|
|
46
|
+
row.uid === 0 &&
|
|
47
|
+
typeof row.originalUid !== "number";
|
|
48
|
+
|
|
49
|
+
type CopyRedrive =
|
|
50
|
+
| { kind: "redrive"; event: Unstamped<MessageCopyEventV3> }
|
|
51
|
+
| { kind: "unresolvable"; reason: string }
|
|
52
|
+
| { kind: "wait" };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The message a uid-0 copy was made from: its id derives from the Message-ID
|
|
56
|
+
* the copy's listing row carries, and the copy's own id derives from it.
|
|
57
|
+
*/
|
|
58
|
+
const copyRedriveFor = async (
|
|
59
|
+
client: StrandedPlacementRedriveDeps["client"],
|
|
60
|
+
account: AccountItem,
|
|
61
|
+
copy: MessageItem,
|
|
62
|
+
): Promise<CopyRedrive> => {
|
|
63
|
+
const listing = await client.threadMessage.findByMessageId(
|
|
64
|
+
account.accountConfigId,
|
|
65
|
+
copy.messageId,
|
|
66
|
+
);
|
|
67
|
+
if (!listing) return { kind: "unresolvable", reason: "listing row missing" };
|
|
68
|
+
const header = listing.messageIdHeader;
|
|
69
|
+
if (!header || !isValidMessageId(header)) {
|
|
70
|
+
return { kind: "unresolvable", reason: "listing row has no Message-ID" };
|
|
71
|
+
}
|
|
72
|
+
const sourceId = deriveMessageId(account.accountId, header);
|
|
73
|
+
if (deriveCopyMessageId(sourceId, copy.mailboxId) !== copy.messageId) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "unresolvable",
|
|
76
|
+
reason: "copy was not made from the message its Message-ID names",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const [source] = await client.message.get([sourceId]);
|
|
80
|
+
if (!source) return { kind: "unresolvable", reason: "source row missing" };
|
|
81
|
+
if (source.status === MessageStatus.deleted) {
|
|
82
|
+
return { kind: "unresolvable", reason: "source row deleted" };
|
|
83
|
+
}
|
|
84
|
+
if (source.status !== MessageStatus.active) return { kind: "wait" };
|
|
85
|
+
return {
|
|
86
|
+
kind: "redrive",
|
|
87
|
+
event: {
|
|
88
|
+
type: "MESSAGE_COPY",
|
|
89
|
+
schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
|
|
90
|
+
accountId: account.accountId,
|
|
91
|
+
sourceMessageId: source.messageId,
|
|
92
|
+
newMessageId: copy.messageId,
|
|
93
|
+
sourceMailboxId: source.mailboxId,
|
|
94
|
+
destinationMailboxId: copy.mailboxId,
|
|
95
|
+
uid: source.uid,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** A copy with no source to copy from has nothing to bind and nothing to hide. */
|
|
101
|
+
const removeUnresolvableCopy = async (
|
|
102
|
+
deps: StrandedPlacementRedriveDeps,
|
|
103
|
+
account: AccountItem,
|
|
104
|
+
row: MessageItem,
|
|
105
|
+
reason: string,
|
|
106
|
+
now: number,
|
|
107
|
+
): Promise<boolean> => {
|
|
108
|
+
const removed = await deps.client.message.deleteIfPlacement(row.messageId, {
|
|
109
|
+
status: MessageStatus.moving,
|
|
110
|
+
mailboxId: row.mailboxId,
|
|
111
|
+
uid: 0,
|
|
112
|
+
});
|
|
113
|
+
if (!removed) return false;
|
|
114
|
+
deps.log.error(
|
|
115
|
+
{
|
|
116
|
+
alert: "stranded_copy_unresolvable",
|
|
117
|
+
accountId: account.accountId,
|
|
118
|
+
messageId: row.messageId,
|
|
119
|
+
mailboxId: row.mailboxId,
|
|
120
|
+
reason,
|
|
121
|
+
untouchedForMs: now - row.updatedAt,
|
|
122
|
+
},
|
|
123
|
+
"A copy stranded at uid 0 has no source to copy from; the copy row was removed",
|
|
124
|
+
);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const eventFor = async (
|
|
129
|
+
client: StrandedPlacementRedriveDeps["client"],
|
|
130
|
+
account: AccountItem,
|
|
131
|
+
row: MessageItem,
|
|
132
|
+
trashMailboxId: string | undefined,
|
|
133
|
+
): Promise<StrandedPlacementEvent | undefined> => {
|
|
134
|
+
const { accountId, accountConfigId } = account;
|
|
135
|
+
const { messageId } = row;
|
|
136
|
+
|
|
137
|
+
if (row.status === MessageStatus.deleting) {
|
|
138
|
+
return {
|
|
139
|
+
type: "MESSAGE_DELETE",
|
|
140
|
+
schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
|
|
141
|
+
accountId,
|
|
142
|
+
messageId,
|
|
143
|
+
mailboxId: row.mailboxId,
|
|
144
|
+
uid: row.uid,
|
|
145
|
+
operation: "permanent_delete",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
typeof row.originalMailboxId !== "string" ||
|
|
150
|
+
typeof row.originalUid !== "number"
|
|
151
|
+
) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const marker = await client.placementMove.find(messageId);
|
|
156
|
+
if (marker && marker.destinationMailboxId === row.mailboxId) {
|
|
157
|
+
return {
|
|
158
|
+
type: "PLACEMENT_MOVE_PUSH",
|
|
159
|
+
accountId,
|
|
160
|
+
accountConfigId,
|
|
161
|
+
messageId,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (row.mailboxId === trashMailboxId) {
|
|
165
|
+
return {
|
|
166
|
+
type: "MESSAGE_DELETE",
|
|
167
|
+
schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
|
|
168
|
+
accountId,
|
|
169
|
+
messageId,
|
|
170
|
+
mailboxId: row.originalMailboxId,
|
|
171
|
+
uid: row.originalUid,
|
|
172
|
+
operation: "move_to_trash",
|
|
173
|
+
destinationMailboxId: row.mailboxId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
type: "MESSAGE_MOVE",
|
|
178
|
+
schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
|
|
179
|
+
accountId,
|
|
180
|
+
messageId,
|
|
181
|
+
sourceMailboxId: row.originalMailboxId,
|
|
182
|
+
destinationMailboxId: row.mailboxId,
|
|
183
|
+
uid: row.originalUid,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** The claim restarts the row's clock; a folder awaiting a cursor rebuild waits until it is normal. */
|
|
188
|
+
export const redriveStrandedPlacements = async (
|
|
189
|
+
deps: StrandedPlacementRedriveDeps,
|
|
190
|
+
account: AccountItem,
|
|
191
|
+
now: number,
|
|
192
|
+
): Promise<number> => {
|
|
193
|
+
const { client, emit, log } = deps;
|
|
194
|
+
const { accountId } = account;
|
|
195
|
+
|
|
196
|
+
const mailboxes = await client.mailbox.listAllByAccount(accountId);
|
|
197
|
+
const paused = new Set(
|
|
198
|
+
mailboxes
|
|
199
|
+
.filter((mailbox) => isCursorRebuildNeeded(mailbox.cursorState))
|
|
200
|
+
.map((mailbox) => mailbox.mailboxId),
|
|
201
|
+
);
|
|
202
|
+
const stranded = (
|
|
203
|
+
await client.message.listStrandedPlacements(
|
|
204
|
+
mailboxes.map((mailbox) => mailbox.mailboxId),
|
|
205
|
+
now - STRANDED_PLACEMENT_AFTER_MILLIS,
|
|
206
|
+
)
|
|
207
|
+
).filter(
|
|
208
|
+
(row) =>
|
|
209
|
+
!paused.has(row.mailboxId) &&
|
|
210
|
+
!(row.originalMailboxId && paused.has(row.originalMailboxId)),
|
|
211
|
+
);
|
|
212
|
+
if (stranded.length === 0) return 0;
|
|
213
|
+
|
|
214
|
+
const trash = trashMailboxAt(
|
|
215
|
+
await client.mailboxSpecialUse.resolveTrashRole(accountId),
|
|
216
|
+
"resolved",
|
|
217
|
+
);
|
|
218
|
+
const trashMailboxId = trash.allowed ? trash.mailbox.mailboxId : undefined;
|
|
219
|
+
|
|
220
|
+
let redriven = 0;
|
|
221
|
+
for (const row of stranded) {
|
|
222
|
+
const copy = isStrandedCopy(row)
|
|
223
|
+
? await copyRedriveFor(client, account, row)
|
|
224
|
+
: undefined;
|
|
225
|
+
if (copy?.kind === "wait") continue;
|
|
226
|
+
if (copy?.kind === "unresolvable") {
|
|
227
|
+
if (await removeUnresolvableCopy(deps, account, row, copy.reason, now)) {
|
|
228
|
+
redriven += 1;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const event =
|
|
233
|
+
copy?.kind === "redrive"
|
|
234
|
+
? copy.event
|
|
235
|
+
: await eventFor(client, account, row, trashMailboxId);
|
|
236
|
+
if (!event) continue;
|
|
237
|
+
|
|
238
|
+
// A copy's earlier COPY may have landed with its answer lost, so the
|
|
239
|
+
// re-drive marks it `failed`: the copy handler then asks the destination
|
|
240
|
+
// before it copies again.
|
|
241
|
+
const claimed = await client.message.transitionPlacement(
|
|
242
|
+
row.messageId,
|
|
243
|
+
{ status: row.status, mailboxId: row.mailboxId, uid: row.uid },
|
|
244
|
+
{
|
|
245
|
+
syncStatus: copy ? MessageSyncStatus.failed : MessageSyncStatus.pending,
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
if (!claimed) continue;
|
|
249
|
+
|
|
250
|
+
await emit(event);
|
|
251
|
+
redriven += 1;
|
|
252
|
+
log.warn(
|
|
253
|
+
{
|
|
254
|
+
metric: "stranded_placement_redriven",
|
|
255
|
+
accountId,
|
|
256
|
+
messageId: row.messageId,
|
|
257
|
+
event: event.type,
|
|
258
|
+
syncStatus: row.syncStatus,
|
|
259
|
+
untouchedForMs: now - row.updatedAt,
|
|
260
|
+
},
|
|
261
|
+
"A move or delete had no event left to settle it; re-driven",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return redriven;
|
|
265
|
+
};
|