@remit/smtp-worker 0.0.12 → 0.0.14
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 +1 -1
- package/src/e2e-processor-shim.ts +12 -6
- package/src/failure-kind.test.ts +38 -0
- package/src/failure-kind.ts +16 -0
- package/src/index.ts +18 -8
- package/src/poller.ts +6 -1
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
DeleteMessageCommand,
|
|
4
5
|
ReceiveMessageCommand,
|
|
@@ -46,11 +47,16 @@ process.on("SIGTERM", () => {
|
|
|
46
47
|
isShuttingDown = true;
|
|
47
48
|
});
|
|
48
49
|
|
|
49
|
-
// Minimal Lambda Context: `withTelemetry`
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// Minimal Lambda Context: `withTelemetry` reads `functionName` for the line it
|
|
51
|
+
// logs per invocation and `awsRequestId` for the scope it opens around it, so
|
|
52
|
+
// every line one batch produces shares a `requestId` (deploy/vps/README.md,
|
|
53
|
+
// "Logs"). Minted per batch, like the production poller does — a context built
|
|
54
|
+
// once would put the whole run under one id, which correlates nothing.
|
|
55
|
+
const nextContext = (): Context =>
|
|
56
|
+
({
|
|
57
|
+
functionName: `e2e-smtp-worker-${queueName}`,
|
|
58
|
+
awsRequestId: randomUUID(),
|
|
59
|
+
}) as Context;
|
|
54
60
|
|
|
55
61
|
const pollQueue = async (): Promise<void> => {
|
|
56
62
|
log.info("Worker started, polling...", { maxMessages });
|
|
@@ -115,7 +121,7 @@ const pollQueue = async (): Promise<void> => {
|
|
|
115
121
|
})),
|
|
116
122
|
};
|
|
117
123
|
|
|
118
|
-
const result = (await handler(event,
|
|
124
|
+
const result = (await handler(event, nextContext())) as
|
|
119
125
|
| SQSBatchResponse
|
|
120
126
|
| undefined;
|
|
121
127
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { RefreshTokenError } from "@remit/mail-oauth-service";
|
|
4
|
+
import { SmtpConnectionError } from "@remit/smtp-service";
|
|
5
|
+
import { smtpFailureKind } from "./failure-kind.js";
|
|
6
|
+
|
|
7
|
+
describe("smtpFailureKind", () => {
|
|
8
|
+
it("counts an SMTP authentication rejection as auth", () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
smtpFailureKind(new SmtpConnectionError("auth", "535 bad credentials")),
|
|
11
|
+
"auth",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("counts a token refresh that cannot mint credentials as auth", () => {
|
|
16
|
+
assert.equal(
|
|
17
|
+
smtpFailureKind(
|
|
18
|
+
new RefreshTokenError({
|
|
19
|
+
kind: "reauth-required",
|
|
20
|
+
code: "invalid_grant",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
"auth",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps a network failure apart from an auth failure", () => {
|
|
28
|
+
assert.equal(
|
|
29
|
+
smtpFailureKind(new SmtpConnectionError("network", "ECONNREFUSED")),
|
|
30
|
+
"network",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("classifies anything else as other", () => {
|
|
35
|
+
assert.equal(smtpFailureKind(new Error("nodemailer blew up")), "other");
|
|
36
|
+
assert.equal(smtpFailureKind("not an error"), "other");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MailFailureKind } from "@remit/logger-lambda";
|
|
2
|
+
import { RefreshTokenError } from "@remit/mail-oauth-service";
|
|
3
|
+
import { SmtpConnectionError } from "@remit/smtp-service";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which class of failure ended an SMTP send, for the exported failure counter
|
|
7
|
+
* (standalone-observability D3). `auth` is its own kind because it is the one
|
|
8
|
+
* class that never resolves itself: an expired OAuth grant or a changed
|
|
9
|
+
* password fails identically forever, and a mailbox that cannot send stays
|
|
10
|
+
* unable to send until someone re-authorises it.
|
|
11
|
+
*/
|
|
12
|
+
export const smtpFailureKind = (error: unknown): MailFailureKind => {
|
|
13
|
+
if (error instanceof RefreshTokenError) return "auth";
|
|
14
|
+
if (error instanceof SmtpConnectionError) return error.kind;
|
|
15
|
+
return "other";
|
|
16
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { inspect } from "node:util";
|
|
2
2
|
import {
|
|
3
3
|
createLogger,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
queueNameFromEventSource,
|
|
5
|
+
recordQueueEvent,
|
|
6
|
+
recordSmtpFailure,
|
|
6
7
|
withTelemetry,
|
|
7
8
|
} from "@remit/logger-lambda";
|
|
8
9
|
import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
|
|
9
10
|
import type { SmtpEvent } from "./events.js";
|
|
11
|
+
import { smtpFailureKind } from "./failure-kind.js";
|
|
10
12
|
import { processEvent } from "./processor.js";
|
|
11
13
|
|
|
12
14
|
const log = createLogger();
|
|
@@ -22,14 +24,16 @@ export const handler = withTelemetry(
|
|
|
22
24
|
eventId: smtpEvent.eventId,
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
const queue = queueNameFromEventSource(record.eventSourceARN);
|
|
25
28
|
const sendStart = Date.now();
|
|
26
29
|
const failed = await processEvent(smtpEvent, log)
|
|
27
30
|
.then(() => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
recordQueueEvent({
|
|
32
|
+
queue,
|
|
33
|
+
eventType: smtpEvent.type,
|
|
34
|
+
outcome: "success",
|
|
35
|
+
durationMs: Date.now() - sendStart,
|
|
36
|
+
});
|
|
33
37
|
return false;
|
|
34
38
|
})
|
|
35
39
|
.catch((error) => {
|
|
@@ -37,7 +41,13 @@ export const handler = withTelemetry(
|
|
|
37
41
|
error: inspect(error),
|
|
38
42
|
messageId: record.messageId,
|
|
39
43
|
});
|
|
40
|
-
|
|
44
|
+
recordQueueEvent({
|
|
45
|
+
queue,
|
|
46
|
+
eventType: smtpEvent.type,
|
|
47
|
+
outcome: "failure",
|
|
48
|
+
durationMs: Date.now() - sendStart,
|
|
49
|
+
});
|
|
50
|
+
recordSmtpFailure(smtpFailureKind(error));
|
|
41
51
|
return true;
|
|
42
52
|
});
|
|
43
53
|
|
package/src/poller.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLogger } from "@remit/logger-lambda";
|
|
1
|
+
import { createLogger, startMetricsServer } from "@remit/logger-lambda";
|
|
2
2
|
import { runQueuePoller } from "@remit/sqs-client/poller";
|
|
3
3
|
import { env } from "expect-env";
|
|
4
4
|
import { handler } from "./index.js";
|
|
@@ -6,6 +6,11 @@ import { handler } from "./index.js";
|
|
|
6
6
|
/** Production queue poller — the deployed form of `e2e-processor-shim.ts`. */
|
|
7
7
|
const log = createLogger();
|
|
8
8
|
|
|
9
|
+
// /metrics and nothing else, on the compose network (standalone-observability
|
|
10
|
+
// D2). No health route on it: worker liveness is a heartbeat file, which keeps
|
|
11
|
+
// answering when this server does not.
|
|
12
|
+
startMetricsServer();
|
|
13
|
+
|
|
9
14
|
await runQueuePoller({
|
|
10
15
|
log,
|
|
11
16
|
targets: [
|