@remit/imap-worker 0.0.88 → 0.0.90
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/package.json +6 -6
- package/src/cli.ts +3 -2
- package/src/credential-less-sync-fence.test.ts +5 -9
- package/src/e2e-processor-shim.ts +2 -2
- package/src/emit.test.ts +25 -42
- package/src/emit.ts +94 -119
- package/src/events.ts +1 -19
- package/src/handlers/delete-account-objects.test.ts +27 -18
- package/src/handlers/delete-account-objects.ts +5 -12
- package/src/handlers/message-copy.sqlite.test.ts +3 -3
- package/src/handlers/message-copy.ts +5 -2
- package/src/handlers/message-delete.sqlite.test.ts +1 -1
- package/src/handlers/message-delete.ts +7 -4
- package/src/handlers/message-move.sqlite.test.ts +1 -1
- package/src/handlers/message-move.ts +9 -6
- package/src/handlers/placement-move-push.ts +9 -6
- package/src/handlers/placement-push.sqlite.test.ts +3 -2
- package/src/handlers/probe-message-id.sqlite.test.ts +2 -4
- package/src/handlers/sync-mailboxes.ts +8 -4
- package/src/handlers/sync-message-body.sqlite.test.ts +2 -2
- package/src/handlers/sync-message-body.ts +15 -18
- package/src/handlers/sync-messages.sqlite.test.ts +14 -2
- package/src/handlers/sync-messages.ts +20 -8
- package/src/index.ts +3 -1
- package/src/mail-sync-selection.sqlite.test.ts +7 -13
- package/src/pending-sync.sqlite.test.ts +26 -28
- package/src/processor.ts +10 -16
- package/src/queues.ts +40 -0
- package/src/scheduler/attachment-sweep.sqlite.test.ts +2 -2
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +20 -27
- package/src/scheduler/handler.ts +35 -28
- package/src/scheduler/run-tick.sqlite.test.ts +6 -14
- package/src/scheduler/run-tick.ts +10 -14
- package/src/scheduler/runner.ts +15 -12
- package/src/stranded-placement.sqlite.test.ts +9 -5
- package/src/test-helpers/test-world.ts +40 -13
- package/src/test-helpers/worker-queues.ts +11 -0
- package/src/processor.test.ts +0 -58
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
4
|
-
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
5
4
|
import {
|
|
6
5
|
_resetForTest,
|
|
7
6
|
type RemitClient,
|
|
8
7
|
setClient,
|
|
9
8
|
} from "@remit/backend/client";
|
|
10
9
|
import type { AccountItem } from "@remit/data-ports";
|
|
10
|
+
import { recordingQueue } from "@remit/data-ports/recording-queue";
|
|
11
11
|
import {
|
|
12
12
|
AccountAuthType,
|
|
13
13
|
AccountService,
|
|
@@ -26,6 +26,7 @@ import type { Logger } from "@remit/logger";
|
|
|
26
26
|
import { syncMailboxes } from "./handlers/sync-mailboxes.js";
|
|
27
27
|
import { syncMessages } from "./handlers/sync-messages.js";
|
|
28
28
|
import { runSchedulerTick } from "./scheduler/run-tick.js";
|
|
29
|
+
import { workerQueuesOn } from "./test-helpers/worker-queues.js";
|
|
29
30
|
|
|
30
31
|
const NOW = 1_700_000_000_000;
|
|
31
32
|
|
|
@@ -130,18 +131,11 @@ const seedSyncedAccount = async (): Promise<{
|
|
|
130
131
|
};
|
|
131
132
|
|
|
132
133
|
const tick = async (): Promise<{ enqueued: number; accountIds: string[] }> => {
|
|
133
|
-
const
|
|
134
|
-
const sqsClient = {
|
|
135
|
-
send: async (command: SendMessageCommand) => {
|
|
136
|
-
sent.push(command);
|
|
137
|
-
return {};
|
|
138
|
-
},
|
|
139
|
-
} as unknown as SQSClient;
|
|
134
|
+
const queue = recordingQueue();
|
|
140
135
|
const result = await runSchedulerTick({
|
|
141
136
|
redriveStrandedPlacements: async () => 0,
|
|
142
137
|
accountService: accounts,
|
|
143
|
-
|
|
144
|
-
queueUrl: "https://queue.test/mailboxes",
|
|
138
|
+
queue: queue.fifo,
|
|
145
139
|
log: silentLogger,
|
|
146
140
|
tickIntervalMs: 60_000,
|
|
147
141
|
offlineIntervalMs: 0,
|
|
@@ -149,9 +143,7 @@ const tick = async (): Promise<{ enqueued: number; accountIds: string[] }> => {
|
|
|
149
143
|
});
|
|
150
144
|
return {
|
|
151
145
|
enqueued: result.enqueued,
|
|
152
|
-
accountIds: sent.map(
|
|
153
|
-
(command) => JSON.parse(command.input.MessageBody ?? "{}").accountId,
|
|
154
|
-
),
|
|
146
|
+
accountIds: queue.sent.map((message) => String(message.body.accountId)),
|
|
155
147
|
};
|
|
156
148
|
};
|
|
157
149
|
|
|
@@ -178,6 +170,7 @@ describe("turning mail sync off and on again", () => {
|
|
|
178
170
|
accountId: account.accountId,
|
|
179
171
|
},
|
|
180
172
|
silentLogger,
|
|
173
|
+
workerQueuesOn(recordingQueue()),
|
|
181
174
|
);
|
|
182
175
|
|
|
183
176
|
let connections = 0;
|
|
@@ -190,6 +183,7 @@ describe("turning mail sync off and on again", () => {
|
|
|
190
183
|
mailboxId,
|
|
191
184
|
},
|
|
192
185
|
silentLogger,
|
|
186
|
+
workerQueuesOn(recordingQueue()),
|
|
193
187
|
() => {
|
|
194
188
|
connections += 1;
|
|
195
189
|
throw new Error("a sync that is off must not connect");
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach,
|
|
3
|
-
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
4
3
|
import {
|
|
5
4
|
_resetForTest,
|
|
6
5
|
type RemitClient,
|
|
7
6
|
setClient,
|
|
8
7
|
} from "@remit/backend/client";
|
|
8
|
+
import {
|
|
9
|
+
type RecordingQueue,
|
|
10
|
+
recordingQueue,
|
|
11
|
+
} from "@remit/data-ports/recording-queue";
|
|
9
12
|
import { MailboxLockRepo } from "@remit/drizzle-service";
|
|
10
13
|
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
11
|
-
import {
|
|
12
|
-
|
|
14
|
+
import {
|
|
15
|
+
createEmitEvent,
|
|
16
|
+
type EmitEvent,
|
|
17
|
+
SYNC_MESSAGES_PENDING,
|
|
18
|
+
} from "./emit.js";
|
|
13
19
|
import type { SyncMessagesEvent } from "./events.js";
|
|
14
20
|
import { emitMailboxResync, emitMoveResync } from "./handlers/message-move.js";
|
|
15
21
|
import { emitSyncMessagesEvents } from "./handlers/sync-mailboxes.js";
|
|
22
|
+
import { workerQueuesOn } from "./test-helpers/worker-queues.js";
|
|
16
23
|
|
|
17
24
|
const ACCOUNT = "account-1366";
|
|
18
25
|
const FOLDERS = [
|
|
@@ -21,20 +28,17 @@ const FOLDERS = [
|
|
|
21
28
|
{ mailboxId: "mbx-archive", fullPath: "Archive" },
|
|
22
29
|
];
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let emitEvent: typeof Emit.emitEvent;
|
|
27
|
-
let SYNC_MESSAGES_PENDING: typeof Emit.SYNC_MESSAGES_PENDING;
|
|
31
|
+
let queue: RecordingQueue;
|
|
28
32
|
let locks: MailboxLockRepo;
|
|
29
33
|
let closeStore: () => void;
|
|
30
34
|
|
|
31
35
|
type Queued = { mailboxId: string; eventId: string };
|
|
32
36
|
|
|
33
37
|
const queued = (): Queued[] =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
+
queue.sent.map(({ body }) => ({
|
|
39
|
+
mailboxId: String(body.mailboxId),
|
|
40
|
+
eventId: String(body.eventId),
|
|
41
|
+
}));
|
|
38
42
|
|
|
39
43
|
const inboxSync = (): Omit<SyncMessagesEvent, "eventId" | "timestamp"> => ({
|
|
40
44
|
type: "SYNC_MESSAGES",
|
|
@@ -42,21 +46,14 @@ const inboxSync = (): Omit<SyncMessagesEvent, "eventId" | "timestamp"> => ({
|
|
|
42
46
|
mailboxId: "mbx-inbox",
|
|
43
47
|
});
|
|
44
48
|
|
|
49
|
+
const emitEvent: EmitEvent = (event) =>
|
|
50
|
+
createEmitEvent(workerQueuesOn(queue))(event);
|
|
51
|
+
|
|
45
52
|
const fanOut = (): Promise<void> =>
|
|
46
53
|
emitSyncMessagesEvents(ACCOUNT, FOLDERS, emitEvent);
|
|
47
54
|
|
|
48
|
-
before(async () => {
|
|
49
|
-
const fifo = (name: string) =>
|
|
50
|
-
`https://sqs.eu-west-1.amazonaws.com/0/test-${name}.fifo`;
|
|
51
|
-
process.env.SQS_QUEUE_URL_MAILBOXES = fifo("mailboxes");
|
|
52
|
-
process.env.SQS_QUEUE_URL_MESSAGES = fifo("messages");
|
|
53
|
-
process.env.SQS_QUEUE_URL_FLAGS = fifo("flags");
|
|
54
|
-
({ emitEvent, SYNC_MESSAGES_PENDING } = await import("./emit.js"));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
55
|
beforeEach(() => {
|
|
58
|
-
|
|
59
|
-
sqsMock.on(SendMessageCommand).resolves({});
|
|
56
|
+
queue = recordingQueue();
|
|
60
57
|
const store = createShippedSqliteDb();
|
|
61
58
|
closeStore = store.close;
|
|
62
59
|
locks = new MailboxLockRepo(store.db);
|
|
@@ -140,10 +137,11 @@ describe("SYNC_MESSAGES under explicit refreshes (#1366)", () => {
|
|
|
140
137
|
});
|
|
141
138
|
|
|
142
139
|
it("releases the marker when the send fails, so the next round is not swallowed", async () => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
let sends = 0;
|
|
141
|
+
queue.answer(async () => {
|
|
142
|
+
sends += 1;
|
|
143
|
+
if (sends === 1) throw new Error("queue unreachable");
|
|
144
|
+
});
|
|
147
145
|
|
|
148
146
|
await assert.rejects(emitEvent(inboxSync()), /queue unreachable/);
|
|
149
147
|
await emitEvent(inboxSync());
|
|
@@ -162,6 +160,6 @@ describe("SYNC_MESSAGES under explicit refreshes (#1366)", () => {
|
|
|
162
160
|
} as unknown as RemitClient);
|
|
163
161
|
|
|
164
162
|
await assert.rejects(emitEvent(inboxSync()), /marker store down/);
|
|
165
|
-
assert.equal(
|
|
163
|
+
assert.equal(queue.sent.length, 0);
|
|
166
164
|
});
|
|
167
165
|
});
|
package/src/processor.ts
CHANGED
|
@@ -12,10 +12,12 @@ import { handlePlacementMovePush } from "./handlers/placement-move-push.js";
|
|
|
12
12
|
import { syncMailboxes } from "./handlers/sync-mailboxes.js";
|
|
13
13
|
import { syncMessageBody } from "./handlers/sync-message-body.js";
|
|
14
14
|
import { syncMessages } from "./handlers/sync-messages.js";
|
|
15
|
+
import type { WorkerQueues } from "./queues.js";
|
|
15
16
|
|
|
16
17
|
export const processEvent = async (
|
|
17
18
|
event: WorkerEvent,
|
|
18
19
|
log: Logger,
|
|
20
|
+
queues: WorkerQueues,
|
|
19
21
|
/**
|
|
20
22
|
* SQS's own delivery count for the record carrying this event (1 on first
|
|
21
23
|
* delivery). Read by SYNC_MESSAGE_BODY, PLACEMENT_MOVE_PUSH, FLAG_PUSH,
|
|
@@ -29,39 +31,31 @@ export const processEvent = async (
|
|
|
29
31
|
): Promise<void> => {
|
|
30
32
|
switch (event.type) {
|
|
31
33
|
case "SYNC_MAILBOXES":
|
|
32
|
-
return syncMailboxes(event, log);
|
|
34
|
+
return syncMailboxes(event, log, queues);
|
|
33
35
|
case "SYNC_MESSAGES":
|
|
34
|
-
return syncMessages(event, log);
|
|
36
|
+
return syncMessages(event, log, queues);
|
|
35
37
|
case "SYNC_MESSAGE_BODY":
|
|
36
|
-
return syncMessageBody(event, log, receiveCount);
|
|
38
|
+
return syncMessageBody(event, log, queues, receiveCount);
|
|
37
39
|
case "MAILBOX_CREATE":
|
|
38
40
|
case "MAILBOX_RENAME":
|
|
39
41
|
case "MAILBOX_DELETE":
|
|
40
42
|
return processMailboxManagement(event, log);
|
|
41
43
|
case "MESSAGE_DELETE":
|
|
42
|
-
return handleMessageDelete(event, log, receiveCount);
|
|
44
|
+
return handleMessageDelete(event, log, queues, receiveCount);
|
|
43
45
|
case "MESSAGE_MOVE":
|
|
44
|
-
return handleMessageMove(event, log, receiveCount);
|
|
46
|
+
return handleMessageMove(event, log, queues, receiveCount);
|
|
45
47
|
case "PLACEMENT_MOVE_PUSH":
|
|
46
|
-
return handlePlacementMovePush(event, log, receiveCount);
|
|
48
|
+
return handlePlacementMovePush(event, log, queues, receiveCount);
|
|
47
49
|
case "FLAG_PUSH":
|
|
48
50
|
return handleFlagPush(event, log, receiveCount);
|
|
49
51
|
case "MESSAGE_COPY":
|
|
50
|
-
return handleMessageCopy(event, log, receiveCount);
|
|
52
|
+
return handleMessageCopy(event, log, queues, receiveCount);
|
|
51
53
|
case "EMPTY_TRASH":
|
|
52
54
|
return handleEmptyTrash(event, log);
|
|
53
55
|
case "APPEND_SENT_MESSAGE":
|
|
54
56
|
return handleAppendSentMessage(event, log, receiveCount);
|
|
55
57
|
case "DELETE_ACCOUNT_OBJECTS":
|
|
56
|
-
return handleDeleteAccountObjects(event, log);
|
|
57
|
-
case "IMAP_WORKER_STOP":
|
|
58
|
-
// Tombstone fence on the account row already stops processing;
|
|
59
|
-
// this event acks the cascade contract and is a no-op today.
|
|
60
|
-
log.info(
|
|
61
|
-
{ accountConfigId: event.accountConfigId, accountId: event.accountId },
|
|
62
|
-
"Imap worker stop signal received",
|
|
63
|
-
);
|
|
64
|
-
return;
|
|
58
|
+
return handleDeleteAccountObjects(event, log, queues);
|
|
65
59
|
default:
|
|
66
60
|
throw new Error(`Unknown event type: ${(event as WorkerEvent).type}`);
|
|
67
61
|
}
|
package/src/queues.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FifoQueueProducer, QueueProducer } from "@remit/data-ports";
|
|
2
|
+
import {
|
|
3
|
+
createFifoQueueProducer,
|
|
4
|
+
createStandardQueueProducer,
|
|
5
|
+
} from "@remit/sqs-client/producer";
|
|
6
|
+
import { env } from "expect-env";
|
|
7
|
+
import type { ImapEvent, WorkerEvent } from "./events.js";
|
|
8
|
+
|
|
9
|
+
export interface WorkerQueues {
|
|
10
|
+
mailboxes: FifoQueueProducer<ImapEvent>;
|
|
11
|
+
messages: FifoQueueProducer<ImapEvent>;
|
|
12
|
+
flags: FifoQueueProducer<ImapEvent>;
|
|
13
|
+
body: QueueProducer<ImapEvent>;
|
|
14
|
+
mailboxManagement: QueueProducer<ImapEvent>;
|
|
15
|
+
messageManagement: QueueProducer<WorkerEvent>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WorkerQueueUrls = Record<keyof WorkerQueues, string>;
|
|
19
|
+
|
|
20
|
+
export const workerQueueUrlsFromEnv = (): WorkerQueueUrls => ({
|
|
21
|
+
mailboxes: env.SQS_QUEUE_URL_MAILBOXES,
|
|
22
|
+
messages: env.SQS_QUEUE_URL_MESSAGES,
|
|
23
|
+
flags: env.SQS_QUEUE_URL_FLAGS,
|
|
24
|
+
body: env.SQS_QUEUE_URL_BODY,
|
|
25
|
+
mailboxManagement: env.SQS_QUEUE_URL_MAILBOX_MGMT,
|
|
26
|
+
messageManagement: env.SQS_QUEUE_URL_MESSAGE_MGMT,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const sqsWorkerQueues = (urls: WorkerQueueUrls): WorkerQueues => ({
|
|
30
|
+
mailboxes: createFifoQueueProducer({ queueUrl: urls.mailboxes }),
|
|
31
|
+
messages: createFifoQueueProducer({ queueUrl: urls.messages }),
|
|
32
|
+
flags: createFifoQueueProducer({ queueUrl: urls.flags }),
|
|
33
|
+
body: createStandardQueueProducer({ queueUrl: urls.body }),
|
|
34
|
+
mailboxManagement: createStandardQueueProducer({
|
|
35
|
+
queueUrl: urls.mailboxManagement,
|
|
36
|
+
}),
|
|
37
|
+
messageManagement: createStandardQueueProducer({
|
|
38
|
+
queueUrl: urls.messageManagement,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
@@ -57,8 +57,8 @@ describe("the outbox attachment sweep", () => {
|
|
|
57
57
|
const second = await store.world();
|
|
58
58
|
const secondDraft = await arrange(second);
|
|
59
59
|
|
|
60
|
-
const {
|
|
61
|
-
await
|
|
60
|
+
const { buildScheduledHandler } = await import("./handler.js");
|
|
61
|
+
await buildScheduledHandler(second.queues)(
|
|
62
62
|
{ time: new Date().toISOString() } as never,
|
|
63
63
|
{} as never,
|
|
64
64
|
() => {},
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { after, before, beforeEach, describe, it } from "node:test";
|
|
4
|
-
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
5
4
|
import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
|
|
6
5
|
import type { RemitClient } from "@remit/backend/client";
|
|
7
6
|
import { importConfig } from "@remit/config-transfer";
|
|
8
7
|
import type { AccountItem } from "@remit/data-ports";
|
|
8
|
+
import {
|
|
9
|
+
failingQueue,
|
|
10
|
+
type RecordingQueue,
|
|
11
|
+
recordingQueue,
|
|
12
|
+
} from "@remit/data-ports/recording-queue";
|
|
9
13
|
import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
|
|
10
14
|
import { noopLogger } from "@remit/logger";
|
|
11
15
|
import { openStore } from "../test-helpers/test-world.js";
|
|
12
16
|
|
|
13
|
-
const FANOUT_URL = "http://queue:9324/000000000000/remit-account-fanout";
|
|
14
|
-
|
|
15
17
|
let store: Awaited<ReturnType<typeof openStore>>;
|
|
16
18
|
let client: RemitClient;
|
|
17
19
|
|
|
@@ -105,21 +107,11 @@ const importAccount = async (email: string): Promise<AccountItem> => {
|
|
|
105
107
|
return client.account.get(accountId);
|
|
106
108
|
};
|
|
107
109
|
|
|
108
|
-
const
|
|
109
|
-
({
|
|
110
|
-
send: async (command: SendMessageCommand) => {
|
|
111
|
-
if (fail) throw new Error("queue unreachable");
|
|
112
|
-
sent.push(command);
|
|
113
|
-
return {};
|
|
114
|
-
},
|
|
115
|
-
}) as unknown as SQSClient;
|
|
116
|
-
|
|
117
|
-
const heal = (sqsClient: SQSClient) =>
|
|
110
|
+
const heal = (queue: RecordingQueue) =>
|
|
118
111
|
finishHalfDeletedAccounts({
|
|
119
112
|
account: client.account,
|
|
120
113
|
configImport: client.configImport,
|
|
121
|
-
|
|
122
|
-
accountFanoutQueueUrl: FANOUT_URL,
|
|
114
|
+
queue,
|
|
123
115
|
log: noopLogger,
|
|
124
116
|
});
|
|
125
117
|
|
|
@@ -150,13 +142,11 @@ describe("finishing an account deleted before #1384 on scheduler start", () => {
|
|
|
150
142
|
]);
|
|
151
143
|
|
|
152
144
|
it("tombstones it and asks for the same purge a delete asks for", async () => {
|
|
153
|
-
const
|
|
145
|
+
const queue = recordingQueue();
|
|
154
146
|
|
|
155
|
-
await heal(
|
|
147
|
+
await heal(queue);
|
|
156
148
|
|
|
157
|
-
const purges = sent
|
|
158
|
-
.filter((command) => command.input.QueueUrl === FANOUT_URL)
|
|
159
|
-
.map((command) => JSON.parse(command.input.MessageBody ?? "{}"));
|
|
149
|
+
const purges = queue.sent.map((message) => message.body);
|
|
160
150
|
assert.ok(
|
|
161
151
|
purges.some(
|
|
162
152
|
(body) =>
|
|
@@ -175,27 +165,30 @@ describe("finishing an account deleted before #1384 on scheduler start", () => {
|
|
|
175
165
|
assert.ok(
|
|
176
166
|
!purges.some((body) =>
|
|
177
167
|
[live.accountId, imported.accountId, deleted.accountId].includes(
|
|
178
|
-
body.accountId,
|
|
168
|
+
String(body.accountId),
|
|
179
169
|
),
|
|
180
170
|
),
|
|
181
171
|
);
|
|
182
172
|
});
|
|
183
173
|
|
|
184
174
|
it("does nothing for it on the next start", async () => {
|
|
185
|
-
await heal(
|
|
186
|
-
const
|
|
175
|
+
await heal(recordingQueue());
|
|
176
|
+
const queue = recordingQueue();
|
|
187
177
|
|
|
188
|
-
await heal(
|
|
178
|
+
await heal(queue);
|
|
189
179
|
|
|
190
180
|
assert.ok(
|
|
191
|
-
!sent.some(
|
|
192
|
-
(
|
|
181
|
+
!queue.sent.some(
|
|
182
|
+
(message) => message.body.accountId === halfDeleted.accountId,
|
|
193
183
|
),
|
|
194
184
|
);
|
|
195
185
|
});
|
|
196
186
|
|
|
197
187
|
it("fails the start and leaves it to be found again when the purge cannot be asked for", async () => {
|
|
198
|
-
await assert.rejects(
|
|
188
|
+
await assert.rejects(
|
|
189
|
+
heal(failingQueue(new Error("queue unreachable"))),
|
|
190
|
+
/queue unreachable/,
|
|
191
|
+
);
|
|
199
192
|
|
|
200
193
|
const after = await client.account.get(halfDeleted.accountId);
|
|
201
194
|
assert.equal(after.deletedAt, undefined);
|
package/src/scheduler/handler.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { getClient } from "@remit/backend/client";
|
|
2
2
|
import { createLogger } from "@remit/logger";
|
|
3
3
|
import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
4
|
-
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
5
4
|
import type { ScheduledHandler } from "aws-lambda";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { createEmitEvent } from "../emit.js";
|
|
6
|
+
import {
|
|
7
|
+
sqsWorkerQueues,
|
|
8
|
+
type WorkerQueues,
|
|
9
|
+
workerQueueUrlsFromEnv,
|
|
10
|
+
} from "../queues.js";
|
|
8
11
|
import { buildAttachmentSweep } from "./attachment-sweep.js";
|
|
9
12
|
import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
|
|
10
13
|
import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
|
|
@@ -15,9 +18,6 @@ installNodeLogger();
|
|
|
15
18
|
|
|
16
19
|
const log = createLogger();
|
|
17
20
|
|
|
18
|
-
const mailboxesQueueUrl = env.SQS_QUEUE_URL_MAILBOXES;
|
|
19
|
-
const sqsClient = createQueueProducer({ queueUrl: mailboxesQueueUrl });
|
|
20
|
-
|
|
21
21
|
/**
|
|
22
22
|
* EventBridge-scheduled entry point for the periodic mailbox-sync tick
|
|
23
23
|
* (#1247, restructured #1251). Ticks at `MAILBOX_SYNC_TICK_INTERVAL_SECONDS`
|
|
@@ -35,28 +35,35 @@ const sqsClient = createQueueProducer({ queueUrl: mailboxesQueueUrl });
|
|
|
35
35
|
* while a genuine retry of the same invocation carries the same `time` and
|
|
36
36
|
* correctly dedupes.
|
|
37
37
|
*/
|
|
38
|
-
export const
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
export const buildScheduledHandler = (queues: WorkerQueues): ScheduledHandler =>
|
|
39
|
+
withTelemetry(async (event) => {
|
|
40
|
+
const client = await getClient();
|
|
41
|
+
const now = Date.parse(event.time);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
redriveStrandedPlacements(
|
|
57
|
-
{ client, emit: emitEvent, log },
|
|
58
|
-
account,
|
|
59
|
-
tickNow,
|
|
43
|
+
await runSchedulerTick({
|
|
44
|
+
accountService: client.account,
|
|
45
|
+
queue: queues.mailboxes,
|
|
46
|
+
log,
|
|
47
|
+
tickIntervalMs: getTickIntervalMs(),
|
|
48
|
+
offlineIntervalMs: getOfflineIntervalMs(),
|
|
49
|
+
now,
|
|
50
|
+
// The hosted side is the one that needs this: a browser PUTs straight
|
|
51
|
+
// to block storage, so nothing refuses an upload against a draft that is
|
|
52
|
+
// already gone and this sweep is the only thing that collects it.
|
|
53
|
+
sweepAttachments: buildAttachmentSweep(client, log),
|
|
54
|
+
refreshCalendarSubscriptions: buildCalendarSubscriptionRefresh(
|
|
55
|
+
client,
|
|
56
|
+
log,
|
|
60
57
|
),
|
|
58
|
+
redriveStrandedPlacements: (account, tickNow) =>
|
|
59
|
+
redriveStrandedPlacements(
|
|
60
|
+
{ client, emit: createEmitEvent(queues), log },
|
|
61
|
+
account,
|
|
62
|
+
tickNow,
|
|
63
|
+
),
|
|
64
|
+
});
|
|
61
65
|
});
|
|
62
|
-
|
|
66
|
+
|
|
67
|
+
export const handler: ScheduledHandler = buildScheduledHandler(
|
|
68
|
+
sqsWorkerQueues(workerQueueUrlsFromEnv()),
|
|
69
|
+
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
4
3
|
import type { AccountItem } from "@remit/data-ports";
|
|
4
|
+
import { recordingQueue } from "@remit/data-ports/recording-queue";
|
|
5
5
|
import {
|
|
6
6
|
AccountAuthType,
|
|
7
7
|
AccountService,
|
|
@@ -60,18 +60,12 @@ const account = async (
|
|
|
60
60
|
const tick = async (
|
|
61
61
|
options: Partial<Parameters<typeof runSchedulerTick>[0]> = {},
|
|
62
62
|
) => {
|
|
63
|
-
const
|
|
63
|
+
const queue = recordingQueue();
|
|
64
|
+
const { sent } = queue;
|
|
64
65
|
const logs: LogLine[] = [];
|
|
65
66
|
const result = await runSchedulerTick({
|
|
66
67
|
accountService: accounts,
|
|
67
|
-
|
|
68
|
-
send: async (command: SendMessageCommand) => {
|
|
69
|
-
sent.push(command);
|
|
70
|
-
return {};
|
|
71
|
-
},
|
|
72
|
-
} as unknown as SQSClient,
|
|
73
|
-
queueUrl:
|
|
74
|
-
"https://sqs.eu-west-1.amazonaws.com/123/remit-dev-mailboxes.fifo",
|
|
68
|
+
queue: queue.fifo,
|
|
75
69
|
log: capturingLogger(logs),
|
|
76
70
|
tickIntervalMs: TICK_INTERVAL_MS,
|
|
77
71
|
offlineIntervalMs: OFFLINE_INTERVAL_MS,
|
|
@@ -79,9 +73,7 @@ const tick = async (
|
|
|
79
73
|
redriveStrandedPlacements: async () => 0,
|
|
80
74
|
...options,
|
|
81
75
|
});
|
|
82
|
-
const enqueued = sent.map(
|
|
83
|
-
(command) => JSON.parse(command.input.MessageBody ?? "{}").accountId,
|
|
84
|
-
);
|
|
76
|
+
const enqueued = sent.map((message) => String(message.body.accountId));
|
|
85
77
|
return { result, sent, enqueued, logs };
|
|
86
78
|
};
|
|
87
79
|
|
|
@@ -129,7 +121,7 @@ describe("a scheduler tick", () => {
|
|
|
129
121
|
|
|
130
122
|
const { sent } = await tick();
|
|
131
123
|
|
|
132
|
-
const dedupId = sent[0]?.
|
|
124
|
+
const dedupId = sent[0]?.deduplicationId;
|
|
133
125
|
assert.ok(
|
|
134
126
|
dedupId?.startsWith(`SYNC_MAILBOXES:scheduled:${live.accountId}:`),
|
|
135
127
|
);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
2
1
|
import {
|
|
3
2
|
buildScheduledSyncDedupId,
|
|
3
|
+
type SyncMailboxesEvent,
|
|
4
4
|
triggerAccountSync,
|
|
5
5
|
} from "@remit/backend/trigger-sync";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AccountItem,
|
|
8
|
+
FifoQueueProducer,
|
|
9
|
+
IAccountRepository,
|
|
10
|
+
} from "@remit/data-ports";
|
|
7
11
|
import type { Logger } from "@remit/logger";
|
|
8
12
|
import { hasStoredCredential } from "@remit/mailbox-service/account-credentials";
|
|
9
13
|
import pMap from "p-map";
|
|
@@ -22,8 +26,7 @@ import { isSyncDue } from "./decide-due.js";
|
|
|
22
26
|
|
|
23
27
|
export interface RunSchedulerTickDeps {
|
|
24
28
|
accountService: Pick<IAccountRepository, "listAllAccountsPage">;
|
|
25
|
-
|
|
26
|
-
queueUrl: string;
|
|
29
|
+
queue: FifoQueueProducer<SyncMailboxesEvent>;
|
|
27
30
|
log: Logger;
|
|
28
31
|
offlineIntervalMs: number;
|
|
29
32
|
/**
|
|
@@ -125,14 +128,8 @@ const isEligible = (account: AccountItem): boolean => {
|
|
|
125
128
|
export const runSchedulerTick = async (
|
|
126
129
|
deps: RunSchedulerTickDeps,
|
|
127
130
|
): Promise<SchedulerTickResult> => {
|
|
128
|
-
const {
|
|
129
|
-
|
|
130
|
-
sqsClient,
|
|
131
|
-
queueUrl,
|
|
132
|
-
log,
|
|
133
|
-
offlineIntervalMs,
|
|
134
|
-
tickIntervalMs,
|
|
135
|
-
} = deps;
|
|
131
|
+
const { accountService, queue, log, offlineIntervalMs, tickIntervalMs } =
|
|
132
|
+
deps;
|
|
136
133
|
const now = deps.now ?? Date.now();
|
|
137
134
|
|
|
138
135
|
let cursor: string | undefined;
|
|
@@ -161,8 +158,7 @@ export const runSchedulerTick = async (
|
|
|
161
158
|
due,
|
|
162
159
|
(account) =>
|
|
163
160
|
triggerAccountSync({
|
|
164
|
-
|
|
165
|
-
queueUrl,
|
|
161
|
+
queue,
|
|
166
162
|
accountId: account.accountId,
|
|
167
163
|
dedupId: buildScheduledSyncDedupId(
|
|
168
164
|
account.accountId,
|
package/src/scheduler/runner.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
|
|
|
4
4
|
import { getClient } from "@remit/backend/client";
|
|
5
5
|
import { createLogger } from "@remit/logger";
|
|
6
6
|
import { installNodeLogger } from "@remit/logger-lambda/node";
|
|
7
|
+
import { backfillSearchText } from "@remit/mailbox-service";
|
|
7
8
|
import { clearHeartbeats, createHeartbeat } from "@remit/sqs-client/heartbeat";
|
|
8
|
-
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { createEmitEvent } from "../emit.js";
|
|
11
|
+
import { sqsWorkerQueues, workerQueueUrlsFromEnv } from "../queues.js";
|
|
12
12
|
import { buildAttachmentSweep } from "./attachment-sweep.js";
|
|
13
13
|
import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
|
|
14
14
|
import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
|
|
@@ -40,9 +40,7 @@ installNodeLogger();
|
|
|
40
40
|
|
|
41
41
|
const log = createLogger();
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
const sqsClient = createQueueProducer({ queueUrl: mailboxesQueueUrl });
|
|
45
|
-
const accountFanoutQueueUrl = env.SQS_QUEUE_URL_ACCOUNT_FANOUT;
|
|
43
|
+
const queues = sqsWorkerQueues(workerQueueUrlsFromEnv());
|
|
46
44
|
|
|
47
45
|
const tickIntervalMs = getTickIntervalMs();
|
|
48
46
|
const offlineIntervalMs = getOfflineIntervalMs();
|
|
@@ -81,16 +79,20 @@ const runLoop = async (): Promise<void> => {
|
|
|
81
79
|
await finishHalfDeletedAccounts({
|
|
82
80
|
account,
|
|
83
81
|
configImport: client.configImport,
|
|
84
|
-
|
|
85
|
-
accountFanoutQueueUrl,
|
|
82
|
+
queue: client.queues.accountFanout,
|
|
86
83
|
log,
|
|
87
84
|
});
|
|
88
|
-
|
|
85
|
+
const searchTextBackfill = backfillSearchText(
|
|
86
|
+
{ messageService: client.message, storageService: client.storage },
|
|
87
|
+
log,
|
|
88
|
+
).then((result) => {
|
|
89
|
+
log.debug({ ...result }, "Search text backfill finished");
|
|
90
|
+
});
|
|
91
|
+
const schedulerLoop = runSchedulerLoop({
|
|
89
92
|
tick: runSchedulerTick,
|
|
90
93
|
tickDeps: {
|
|
91
94
|
accountService: account,
|
|
92
|
-
|
|
93
|
-
queueUrl: mailboxesQueueUrl,
|
|
95
|
+
queue: queues.mailboxes,
|
|
94
96
|
log,
|
|
95
97
|
tickIntervalMs,
|
|
96
98
|
offlineIntervalMs,
|
|
@@ -101,7 +103,7 @@ const runLoop = async (): Promise<void> => {
|
|
|
101
103
|
),
|
|
102
104
|
redriveStrandedPlacements: (stranded, now) =>
|
|
103
105
|
redriveStrandedPlacements(
|
|
104
|
-
{ client, emit:
|
|
106
|
+
{ client, emit: createEmitEvent(queues), log },
|
|
105
107
|
stranded,
|
|
106
108
|
now,
|
|
107
109
|
),
|
|
@@ -110,6 +112,7 @@ const runLoop = async (): Promise<void> => {
|
|
|
110
112
|
onHeartbeatError: onBeatError,
|
|
111
113
|
tickIntervalMs,
|
|
112
114
|
});
|
|
115
|
+
await Promise.all([searchTextBackfill, schedulerLoop]);
|
|
113
116
|
};
|
|
114
117
|
|
|
115
118
|
runLoop()
|