@reventlessdev/reventless-aws 3.0.0-alpha.419 → 3.0.0-alpha.421
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/CHANGELOG.md +18 -0
- package/package.json +9 -9
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +1 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +1 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +1 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs +3 -3
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +4 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +4 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs +1 -1
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +4 -1
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -1
- package/src/adapter/Postgres/PgProjectionFeed.res +4 -1
- package/src/adapter/Postgres/PgProjectionFeed.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +12 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs +14 -14
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +4 -4
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -5
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +3 -3
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +2 -2
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +1 -1
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +18 -1
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +20 -6
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs +1 -1
- package/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs +1 -1
- package/src/util/Util_CognitoGroupUser_Runtime.res.mjs +2 -2
- package/src/util/Util_Cognito_Runtime.res.mjs +1 -1
- package/src/util/Util_DeadLetterHandler.res +138 -0
- package/src/util/Util_DeadLetterHandler.res.mjs +113 -0
- package/src/util/Util_DeadLetterQueue.res +10 -53
- package/src/util/Util_DeadLetterQueue.res.mjs +6 -26
- package/src/util/Util_DynamoDb_Runtime.res.mjs +4 -4
- package/src/util/Util_Kinesis_Runtime.res.mjs +1 -1
- package/src/util/Util_SNS_Runtime.res.mjs +2 -2
- package/src/util/Util_SQS_Runtime.res.mjs +5 -5
- package/tests/Util_DeadLetterHandlerTest.res +78 -0
- package/tests/Util_DeadLetterHandlerTest.res.mjs +101 -0
|
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
|
|
6
6
|
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
7
7
|
import * as StateTopicPublishMjs from "./StateTopicPublish.mjs";
|
|
8
|
+
import * as RowChange$ReventlessCore from "@reventlessdev/reventless-core/src/RowChange.res.mjs";
|
|
8
9
|
import * as QueryDbEntryPoint_Ops$ReventlessAws from "./QueryDbEntryPoint_Ops.res.mjs";
|
|
9
10
|
import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
|
|
10
11
|
|
|
@@ -70,17 +71,29 @@ function withDisplayName(base, stateSchema) {
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function withRowChange(base) {
|
|
75
|
+
return {
|
|
76
|
+
load: base.load,
|
|
77
|
+
loadStream: base.loadStream,
|
|
78
|
+
save: (id, state, saveMode, ttl) => base.save(id, RowChange$ReventlessCore.stamp(state), saveMode, ttl),
|
|
79
|
+
saveBatch: items => base.saveBatch(items.map(param => [
|
|
80
|
+
param[0],
|
|
81
|
+
RowChange$ReventlessCore.stamp(param[1]),
|
|
82
|
+
param[2]
|
|
83
|
+
])),
|
|
84
|
+
count: base.count,
|
|
85
|
+
delete: base.delete,
|
|
86
|
+
deleteBatch: base.deleteBatch
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
function makeQueryDbOps(queryDbTableName, pgConnection, stateTopicName, indexes, subIdField) {
|
|
74
|
-
|
|
75
|
-
return StateTopicPublishMjs.withLiveUpdates(QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, queryDbTableName, indexes, subIdField), {
|
|
91
|
+
return withRowChange(pgConnection !== undefined ? StateTopicPublishMjs.withLiveUpdates(QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, queryDbTableName, indexes, subIdField), {
|
|
76
92
|
endpoint: process.env["APPSYNC_ENDPOINT"],
|
|
77
93
|
region: process.env["AWS_REGION"],
|
|
78
94
|
topicName: stateTopicName,
|
|
79
95
|
subIdField: subIdField
|
|
80
|
-
});
|
|
81
|
-
} else {
|
|
82
|
-
return QueryDbEntryPoint_Ops$ReventlessAws.makeDynamoQueryDbOps(queryDbTableName);
|
|
83
|
-
}
|
|
96
|
+
}) : QueryDbEntryPoint_Ops$ReventlessAws.makeDynamoQueryDbOps(queryDbTableName));
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
export {
|
|
@@ -89,6 +102,7 @@ export {
|
|
|
89
102
|
withLiveUpdates,
|
|
90
103
|
withUnionMemberTypes,
|
|
91
104
|
withDisplayName,
|
|
105
|
+
withRowChange,
|
|
92
106
|
makeQueryDbOps,
|
|
93
107
|
}
|
|
94
108
|
/* DisplayName-Reventless Not a pure module */
|
package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs
CHANGED
|
@@ -60,7 +60,7 @@ function scheduleInput(roleArn, targetArn, setAt, schedule) {
|
|
|
60
60
|
|
|
61
61
|
function failWithoutQueue(action) {
|
|
62
62
|
let err = action + ` not possible: no Queue configured !`;
|
|
63
|
-
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logError("ScheduledPublisher_EventBridgeScheduler_Runtime-ReventlessAws", undefined, err), () => Effect.sync(() => Stdlib_JsError.throwWithMessage(err))));
|
|
63
|
+
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logError("ScheduledPublisher_EventBridgeScheduler_Runtime-ReventlessAws", undefined, undefined, err), () => Effect.sync(() => Stdlib_JsError.throwWithMessage(err))));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function putSchedule(roleArn, targetArn, schedule) {
|
|
@@ -9,7 +9,7 @@ import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src
|
|
|
9
9
|
function sendMessage(channelId, messageBody) {
|
|
10
10
|
return Effect$1.runPromise(Effect$1.catchAll(Effect$1.retry(Effect$1.map(Effect.tryPromise(SQS_Error$ReventlessAws.classify, () => SQS_Helpers$AwsSdk.sendMessage(channelId, messageBody, undefined, undefined, undefined)), () => {}), SQS_Error$ReventlessAws.retrySchedule), err => {
|
|
11
11
|
let msg = SQS_Error$ReventlessAws.message(err);
|
|
12
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_PluginMessage_Runtime-ReventlessAws", undefined, "Failed to send message to channel: " + msg), () => Effect$1.fail(err));
|
|
12
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_PluginMessage_Runtime-ReventlessAws", undefined, undefined, "Failed to send message to channel: " + msg), () => Effect$1.fail(err));
|
|
13
13
|
}));
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ function addUserToGroup(region, userName, groupName, userPoolId) {
|
|
|
21
21
|
return client.send(addUserToGroupCommand);
|
|
22
22
|
}), () => {}), Cognito_Error$ReventlessAws.retrySchedule), err => {
|
|
23
23
|
let msg = Cognito_Error$ReventlessAws.message(err);
|
|
24
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_CognitoGroupUser_Runtime-ReventlessAws", undefined, `addUserToGroup: ` + msg), () => Effect$1.fail(msg));
|
|
24
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_CognitoGroupUser_Runtime-ReventlessAws", undefined, undefined, `addUserToGroup: ` + msg), () => Effect$1.fail(msg));
|
|
25
25
|
}));
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -39,7 +39,7 @@ function removeUserFromGroup(region, userName, groupName, userPoolId) {
|
|
|
39
39
|
return client.send(removeUserFromGroupCommand);
|
|
40
40
|
}), () => {}), Cognito_Error$ReventlessAws.retrySchedule), err => {
|
|
41
41
|
let msg = Cognito_Error$ReventlessAws.message(err);
|
|
42
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_CognitoGroupUser_Runtime-ReventlessAws", undefined, `removeUserFromGroup: ` + msg), () => Effect$1.fail(msg));
|
|
42
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_CognitoGroupUser_Runtime-ReventlessAws", undefined, undefined, `removeUserFromGroup: ` + msg), () => Effect$1.fail(msg));
|
|
43
43
|
}));
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -24,7 +24,7 @@ function signUp(region, userPoolId, userPoolClientId, userName, password) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function signUpIfMissing(region, userPoolId, userPoolClientId, userName, password) {
|
|
27
|
-
return Effect$1.runPromise(Effect$1.catchAll(Effect$1.flatMap(Effect.tryPromise(Cognito_Error$ReventlessAws.classify, () => signUp(region, userPoolId, userPoolClientId, userName, password)), result => EffectLogger$ReventlessCore.logInfo("Util_Cognito_Runtime-ReventlessAws", undefined, `Created User ` + userName + ` ` + Stdlib_Option.getOr(result.UserSub, ""))), err => EffectLogger$ReventlessCore.logInfo("Util_Cognito_Runtime-ReventlessAws", undefined, Cognito_Error$ReventlessAws.message(err))));
|
|
27
|
+
return Effect$1.runPromise(Effect$1.catchAll(Effect$1.flatMap(Effect.tryPromise(Cognito_Error$ReventlessAws.classify, () => signUp(region, userPoolId, userPoolClientId, userName, password)), result => EffectLogger$ReventlessCore.logInfo("Util_Cognito_Runtime-ReventlessAws", undefined, undefined, `Created User ` + userName + ` ` + Stdlib_Option.getOr(result.UserSub, ""))), err => EffectLogger$ReventlessCore.logInfo("Util_Cognito_Runtime-ReventlessAws", undefined, undefined, Cognito_Error$ReventlessAws.message(err))));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The dead-letter function's handler. `Util_DeadLetterQueue` ships this module's compiled file
|
|
2
|
+
// as the function's whole code, so it must compile to a file that imports nothing: it keeps
|
|
3
|
+
// working when the layer or the handlers of a deploy are broken, which is when dead letters
|
|
4
|
+
// arrive. So only the Stdlib's externals (which compile inline) and plain switches here: no
|
|
5
|
+
// Stdlib function such as `Option.getOr`, which compiles to an import, and no `try`, whose
|
|
6
|
+
// compiled form imports the runtime's exception helpers.
|
|
7
|
+
//
|
|
8
|
+
// It logs what arrived, then FAILS the invocation on purpose.
|
|
9
|
+
//
|
|
10
|
+
// Returning success would let SQS delete the message, which is what this handler used to do,
|
|
11
|
+
// and it made a dead letter unobservable twice over: the queue's depth returned to 0, so a
|
|
12
|
+
// depth alarm could never fire, and the function's `Errors` metric stayed at 0, so neither
|
|
13
|
+
// could an error alarm. A plugin whose commands failed every 5 minutes for two days produced
|
|
14
|
+
// 217 dead letters and no signal at all; it was found by a person noticing a stale UI.
|
|
15
|
+
//
|
|
16
|
+
// Failing instead keeps the message on the queue and keeps `Errors` non-zero while the
|
|
17
|
+
// condition lasts. Both are conventional alarm subjects, and a monitoring backend attached
|
|
18
|
+
// through the `DeadLetterSink` seam now has something to attach to.
|
|
19
|
+
//
|
|
20
|
+
// The transport retries by design too: a failed batch returns to the queue and is redelivered
|
|
21
|
+
// until retention expires. A handler that fails forever, on a transport that retries forever,
|
|
22
|
+
// is a loop, and it outlives its cause, because nothing about a fixed bug removes the message
|
|
23
|
+
// that was stranded by it. So what the loop costs is a function of the line, not the count:
|
|
24
|
+
// the first delivery here writes the record's ids, kind, size and attribute names (never its
|
|
25
|
+
// body, which is a command or event and may be about a person), and every redelivery after it
|
|
26
|
+
// writes a single identity line. A stranded message is ~150 KB over a fortnight rather than
|
|
27
|
+
// ~17 MB, small enough that ending the loop early is an operator's job.
|
|
28
|
+
|
|
29
|
+
// How many times a consuming queue delivers a message before it moves it here. Every redrive
|
|
30
|
+
// policy in the framework takes it from here. SQS keeps a message's receive count when it
|
|
31
|
+
// moves it, so a dead letter's first delivery here reports `maxReceiveCount + 1`; a message
|
|
32
|
+
// sent here directly starts at 1.
|
|
33
|
+
let maxReceiveCount = 5
|
|
34
|
+
let firstDeliveryCount = maxReceiveCount + 1
|
|
35
|
+
|
|
36
|
+
type record = {
|
|
37
|
+
messageId?: string,
|
|
38
|
+
body?: string,
|
|
39
|
+
attributes?: dict<string>,
|
|
40
|
+
messageAttributes?: dict<JSON.t>,
|
|
41
|
+
eventSourceARN?: string,
|
|
42
|
+
}
|
|
43
|
+
type event = {@as("Records") records?: array<record>}
|
|
44
|
+
|
|
45
|
+
// The body parsed, or nothing when it is not JSON. A `try` would import the runtime, so the
|
|
46
|
+
// parse is guarded in plain JS.
|
|
47
|
+
let parseOrNull: string => Nullable.t<JSON.t> = %raw(`function (text) {
|
|
48
|
+
try { return JSON.parse(text) } catch (_) { return null }
|
|
49
|
+
}`)
|
|
50
|
+
|
|
51
|
+
// `Option.getOr` would import the Stdlib, so the default is given here.
|
|
52
|
+
let orElse = (value: option<string>, fallback: string) =>
|
|
53
|
+
switch value {
|
|
54
|
+
| Some(v) => v
|
|
55
|
+
| None => fallback
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The command's or event's tag, "sns" for an SNS envelope, else "unknown".
|
|
59
|
+
let kindOf = (body: string): string =>
|
|
60
|
+
switch parseOrNull(body)->Nullable.toOption {
|
|
61
|
+
| Some(JSON.Object(d)) =>
|
|
62
|
+
let tagOf = key =>
|
|
63
|
+
switch d->Dict.get(key) {
|
|
64
|
+
| Some(JSON.Object(inner)) =>
|
|
65
|
+
switch inner->Dict.get("TAG") {
|
|
66
|
+
| Some(JSON.String(tag)) => Some(tag)
|
|
67
|
+
| _ => None
|
|
68
|
+
}
|
|
69
|
+
| _ => None
|
|
70
|
+
}
|
|
71
|
+
switch (tagOf("command"), tagOf("event"), d->Dict.get("Message")) {
|
|
72
|
+
| (Some(tag), _, _) | (None, Some(tag), _) => tag
|
|
73
|
+
| (None, None, Some(_)) => "sns"
|
|
74
|
+
| (None, None, None) => "unknown"
|
|
75
|
+
}
|
|
76
|
+
| _ => "unknown"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let receiveCountOf = (attrs: dict<string>): int =>
|
|
80
|
+
switch attrs->Dict.get("ApproximateReceiveCount") {
|
|
81
|
+
| Some(text) =>
|
|
82
|
+
let n = Float.parseFloat(text)
|
|
83
|
+
Float.isNaN(n) ? 1 : Float.toInt(n)
|
|
84
|
+
| None => 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The line a record writes: the full one on its first delivery here, the short one after.
|
|
88
|
+
let lineOf = (record: record): string => {
|
|
89
|
+
let attrs = switch record.attributes {
|
|
90
|
+
| Some(a) => a
|
|
91
|
+
| None => Dict.make()
|
|
92
|
+
}
|
|
93
|
+
let receiveCount = receiveCountOf(attrs)
|
|
94
|
+
let body = record.body->orElse("")
|
|
95
|
+
let source = switch attrs->Dict.get("DeadLetterQueueSourceArn") {
|
|
96
|
+
| Some(arn) => arn
|
|
97
|
+
| None => record.eventSourceARN->orElse("unknown")
|
|
98
|
+
}
|
|
99
|
+
let identity =
|
|
100
|
+
"messageId=" ++
|
|
101
|
+
record.messageId->orElse("unknown") ++
|
|
102
|
+
" source=" ++
|
|
103
|
+
source ++
|
|
104
|
+
" receiveCount=" ++
|
|
105
|
+
Int.toString(receiveCount) ++
|
|
106
|
+
" bodyBytes=" ++
|
|
107
|
+
Int.toString(String.length(body))
|
|
108
|
+
if receiveCount <= firstDeliveryCount {
|
|
109
|
+
let attributeNames = switch record.messageAttributes {
|
|
110
|
+
| Some(m) => m->Dict.keysToArray
|
|
111
|
+
| None => []
|
|
112
|
+
}
|
|
113
|
+
"DEAD LETTER ITEM: " ++
|
|
114
|
+
identity ++
|
|
115
|
+
" kind=" ++
|
|
116
|
+
kindOf(body) ++
|
|
117
|
+
" attributes=" ++
|
|
118
|
+
attributeNames->Array.join(",")
|
|
119
|
+
} else {
|
|
120
|
+
"DEAD LETTER REDELIVERY: " ++ identity
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let handler = async (event: event): unit => {
|
|
125
|
+
let records = switch event.records {
|
|
126
|
+
| Some(r) => r
|
|
127
|
+
| None => []
|
|
128
|
+
}
|
|
129
|
+
records->Array.forEach(record => Console.error(lineOf(record)))
|
|
130
|
+
JsError.throw(
|
|
131
|
+
JsError.make(
|
|
132
|
+
"Dead-lettered " ++
|
|
133
|
+
Int.toString(
|
|
134
|
+
Array.length(records),
|
|
135
|
+
) ++ " message(s); see DEAD LETTER ITEM above. Failing so the messages are retained and Errors is non-zero.",
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let firstDeliveryCount = 6;
|
|
5
|
+
|
|
6
|
+
let parseOrNull = (function (text) {
|
|
7
|
+
try { return JSON.parse(text) } catch (_) { return null }
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function orElse(value, fallback) {
|
|
11
|
+
if (value !== undefined) {
|
|
12
|
+
return value;
|
|
13
|
+
} else {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function kindOf(body) {
|
|
19
|
+
let match = parseOrNull(body);
|
|
20
|
+
if (match == null) {
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
if (typeof match !== "object" || match === null || Array.isArray(match)) {
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
let tagOf = key => {
|
|
27
|
+
let match$1 = match[key];
|
|
28
|
+
if (match$1 === undefined) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof match$1 !== "object" || match$1 === null || Array.isArray(match$1)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let match$2 = match$1["TAG"];
|
|
35
|
+
if (typeof match$2 === "string") {
|
|
36
|
+
return match$2;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
let match$1 = tagOf("command");
|
|
40
|
+
let match$2 = tagOf("event");
|
|
41
|
+
let match$3 = match["Message"];
|
|
42
|
+
if (match$1 !== undefined) {
|
|
43
|
+
return match$1;
|
|
44
|
+
} else if (match$2 !== undefined) {
|
|
45
|
+
return match$2;
|
|
46
|
+
} else if (match$3 !== undefined) {
|
|
47
|
+
return "sns";
|
|
48
|
+
} else {
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function receiveCountOf(attrs) {
|
|
54
|
+
let text = attrs["ApproximateReceiveCount"];
|
|
55
|
+
if (text === undefined) {
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
let n = parseFloat(text);
|
|
59
|
+
if (Number.isNaN(n)) {
|
|
60
|
+
return 1;
|
|
61
|
+
} else {
|
|
62
|
+
return n | 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function lineOf(record) {
|
|
67
|
+
let a = record.attributes;
|
|
68
|
+
let attrs = a !== undefined ? a : ({});
|
|
69
|
+
let receiveCount = receiveCountOf(attrs);
|
|
70
|
+
let value = record.body;
|
|
71
|
+
let body = value !== undefined ? value : "";
|
|
72
|
+
let arn = attrs["DeadLetterQueueSourceArn"];
|
|
73
|
+
let source;
|
|
74
|
+
if (arn !== undefined) {
|
|
75
|
+
source = arn;
|
|
76
|
+
} else {
|
|
77
|
+
let value$1 = record.eventSourceARN;
|
|
78
|
+
source = value$1 !== undefined ? value$1 : "unknown";
|
|
79
|
+
}
|
|
80
|
+
let value$2 = record.messageId;
|
|
81
|
+
let identity = "messageId=" + (
|
|
82
|
+
value$2 !== undefined ? value$2 : "unknown"
|
|
83
|
+
) + " source=" + source + " receiveCount=" + receiveCount.toString() + " bodyBytes=" + body.length.toString();
|
|
84
|
+
if (receiveCount > firstDeliveryCount) {
|
|
85
|
+
return "DEAD LETTER REDELIVERY: " + identity;
|
|
86
|
+
}
|
|
87
|
+
let m = record.messageAttributes;
|
|
88
|
+
let attributeNames = m !== undefined ? Object.keys(m) : [];
|
|
89
|
+
return "DEAD LETTER ITEM: " + identity + " kind=" + kindOf(body) + " attributes=" + attributeNames.join(",");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handler(event) {
|
|
93
|
+
let r = event.Records;
|
|
94
|
+
let records = r !== undefined ? r : [];
|
|
95
|
+
records.forEach(record => {
|
|
96
|
+
console.error(lineOf(record));
|
|
97
|
+
});
|
|
98
|
+
throw new Error("Dead-lettered " + records.length.toString() + " message(s); see DEAD LETTER ITEM above. Failing so the messages are retained and Errors is non-zero.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let maxReceiveCount = 5;
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
maxReceiveCount,
|
|
105
|
+
firstDeliveryCount,
|
|
106
|
+
parseOrNull,
|
|
107
|
+
orElse,
|
|
108
|
+
kindOf,
|
|
109
|
+
receiveCountOf,
|
|
110
|
+
lineOf,
|
|
111
|
+
handler,
|
|
112
|
+
}
|
|
113
|
+
/* No side effect */
|
|
@@ -17,6 +17,10 @@ let queueTags = queueName =>
|
|
|
17
17
|
// 4-day default can expire it over a long weekend before anyone reads it.
|
|
18
18
|
let retentionSeconds = 14 * 24 * 60 * 60
|
|
19
19
|
|
|
20
|
+
// How many times a consuming queue delivers a message before it moves it here; every redrive
|
|
21
|
+
// policy in the framework takes it from here. Defined beside the handler, which needs it.
|
|
22
|
+
let maxReceiveCount = Util_DeadLetterHandler.maxReceiveCount
|
|
23
|
+
|
|
20
24
|
// A dead letter has no latency requirement, and the handler below fails on
|
|
21
25
|
// purpose — so a message returns to the queue and is redelivered until retention
|
|
22
26
|
// expires. At 180 s that is ~480 redeliveries per message per day, which outlives
|
|
@@ -67,59 +71,12 @@ let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
|
67
71
|
~opts,
|
|
68
72
|
)
|
|
69
73
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// plugin whose commands failed every 5 minutes for two days produced 217 dead
|
|
77
|
-
// letters and no signal at all; it was found by a person noticing a stale UI.
|
|
78
|
-
//
|
|
79
|
-
// Failing instead keeps the message on the queue and keeps `Errors` non-zero
|
|
80
|
-
// while the condition lasts. Both are conventional alarm subjects, and a
|
|
81
|
-
// monitoring backend attached through the `DeadLetterSink` seam below now has
|
|
82
|
-
// something to attach to.
|
|
83
|
-
//
|
|
84
|
-
// What that reasoning did not account for is that the transport retries by
|
|
85
|
-
// design too: a failed batch returns to the queue and is redelivered until
|
|
86
|
-
// retention expires. A handler that fails forever, on a transport that retries
|
|
87
|
-
// forever, is a loop — and it outlives its cause, because nothing about a fixed
|
|
88
|
-
// bug removes the message that was stranded by it. Seven of them were still
|
|
89
|
-
// cycling twelve hours after the fix that made them impossible.
|
|
90
|
-
//
|
|
91
|
-
// What the loop costs is now a function of the line, not the count: the first
|
|
92
|
-
// delivery writes the record's ids, kind, size and attribute names — never its
|
|
93
|
-
// body, which is a command or event and may be about a person — and every
|
|
94
|
-
// redelivery after it costs a single identity line. A stranded
|
|
95
|
-
// message is ~150 KB over a fortnight rather than ~17 MB, which is small enough
|
|
96
|
-
// that ending the loop early is an operator's job rather than the topology's.
|
|
97
|
-
let entryPointCode = `export const handler = async (event) => {
|
|
98
|
-
const records = event?.Records ?? [];
|
|
99
|
-
for (const record of records) {
|
|
100
|
-
const attrs = record?.attributes ?? {};
|
|
101
|
-
const receiveCount = Number(attrs.ApproximateReceiveCount ?? 1);
|
|
102
|
-
const identity =
|
|
103
|
-
"messageId=" + (record?.messageId ?? "unknown") +
|
|
104
|
-
" source=" + (attrs.DeadLetterQueueSourceArn ?? record?.eventSourceARN ?? "unknown") +
|
|
105
|
-
" receiveCount=" + receiveCount +
|
|
106
|
-
" bodyBytes=" + (record?.body?.length ?? 0);
|
|
107
|
-
if (receiveCount <= 1) {
|
|
108
|
-
// Ids, type and size, never the body: a dead letter is a command or event,
|
|
109
|
-
// and its payload may be about a person.
|
|
110
|
-
const body = (() => { try { return JSON.parse(record?.body ?? "null") } catch { return null } })();
|
|
111
|
-
const kind = body?.command?.TAG ?? body?.event?.TAG ?? (body?.Message ? "sns" : "unknown");
|
|
112
|
-
console.error("DEAD LETTER ITEM: " + identity + " kind=" + kind +
|
|
113
|
-
" attributes=" + JSON.stringify(Object.keys(record?.messageAttributes ?? {})));
|
|
114
|
-
} else {
|
|
115
|
-
console.error("DEAD LETTER REDELIVERY: " + identity);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
throw new Error(
|
|
119
|
-
"Dead-lettered " + records.length +
|
|
120
|
-
" message(s); see DEAD LETTER ITEM above. Failing so the messages are retained and Errors is non-zero."
|
|
121
|
-
);
|
|
122
|
-
};`
|
|
74
|
+
// The function's whole code is `Util_DeadLetterHandler`'s compiled file, which imports
|
|
75
|
+
// nothing (see there), shipped as `index.mjs`. It sits beside `Util_Bundle`'s compiled file,
|
|
76
|
+
// so it resolves from there.
|
|
77
|
+
let entryPointCode = NodeFs.readFileSync(
|
|
78
|
+
Util_Bundle.localRequire->NodeModule.requireResolve("./Util_DeadLetterHandler.res.mjs"),
|
|
79
|
+
)
|
|
123
80
|
|
|
124
81
|
let archiveContents: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
|
|
125
82
|
archiveContents->Dict.set(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
3
4
|
import * as Aws from "@pulumi/aws";
|
|
4
5
|
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
5
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
@@ -11,6 +12,7 @@ import * as Monitoring$ReventlessCore from "@reventlessdev/reventless-core/src/a
|
|
|
11
12
|
import * as Util_Bundle$ReventlessAws from "./Util_Bundle.res.mjs";
|
|
12
13
|
import * as Util_Lambda$ReventlessAws from "./Util_Lambda.res.mjs";
|
|
13
14
|
import * as Util_LambdaLogging$ReventlessAws from "./Util_LambdaLogging.res.mjs";
|
|
15
|
+
import * as Util_DeadLetterHandler$ReventlessAws from "./Util_DeadLetterHandler.res.mjs";
|
|
14
16
|
import * as Util_EventSourceMapping$ReventlessAws from "./Util_EventSourceMapping.res.mjs";
|
|
15
17
|
|
|
16
18
|
let name = "DeadLetterQueue";
|
|
@@ -45,32 +47,7 @@ let opts = {
|
|
|
45
47
|
|
|
46
48
|
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name, "Plugin", "Identity", "Plugin", undefined, undefined, undefined, undefined), opts);
|
|
47
49
|
|
|
48
|
-
let entryPointCode =
|
|
49
|
-
const records = event?.Records ?? [];
|
|
50
|
-
for (const record of records) {
|
|
51
|
-
const attrs = record?.attributes ?? {};
|
|
52
|
-
const receiveCount = Number(attrs.ApproximateReceiveCount ?? 1);
|
|
53
|
-
const identity =
|
|
54
|
-
"messageId=" + (record?.messageId ?? "unknown") +
|
|
55
|
-
" source=" + (attrs.DeadLetterQueueSourceArn ?? record?.eventSourceARN ?? "unknown") +
|
|
56
|
-
" receiveCount=" + receiveCount +
|
|
57
|
-
" bodyBytes=" + (record?.body?.length ?? 0);
|
|
58
|
-
if (receiveCount <= 1) {
|
|
59
|
-
// Ids, type and size, never the body: a dead letter is a command or event,
|
|
60
|
-
// and its payload may be about a person.
|
|
61
|
-
const body = (() => { try { return JSON.parse(record?.body ?? "null") } catch { return null } })();
|
|
62
|
-
const kind = body?.command?.TAG ?? body?.event?.TAG ?? (body?.Message ? "sns" : "unknown");
|
|
63
|
-
console.error("DEAD LETTER ITEM: " + identity + " kind=" + kind +
|
|
64
|
-
" attributes=" + JSON.stringify(Object.keys(record?.messageAttributes ?? {})));
|
|
65
|
-
} else {
|
|
66
|
-
console.error("DEAD LETTER REDELIVERY: " + identity);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
throw new Error(
|
|
70
|
-
"Dead-lettered " + records.length +
|
|
71
|
-
" message(s); see DEAD LETTER ITEM above. Failing so the messages are retained and Errors is non-zero."
|
|
72
|
-
);
|
|
73
|
-
};`;
|
|
50
|
+
let entryPointCode = Nodefs.readFileSync(Util_Bundle$ReventlessAws.localRequire.resolve("./Util_DeadLetterHandler.res.mjs"), "utf8");
|
|
74
51
|
|
|
75
52
|
let archiveContents = {};
|
|
76
53
|
|
|
@@ -185,6 +162,8 @@ Pulumi.all([
|
|
|
185
162
|
|
|
186
163
|
let retentionSeconds = 1209600;
|
|
187
164
|
|
|
165
|
+
let maxReceiveCount = Util_DeadLetterHandler$ReventlessAws.maxReceiveCount;
|
|
166
|
+
|
|
188
167
|
let visibilityTimeoutSeconds = 900;
|
|
189
168
|
|
|
190
169
|
export {
|
|
@@ -192,6 +171,7 @@ export {
|
|
|
192
171
|
nameFifo,
|
|
193
172
|
queueTags,
|
|
194
173
|
retentionSeconds,
|
|
174
|
+
maxReceiveCount,
|
|
195
175
|
visibilityTimeoutSeconds,
|
|
196
176
|
queue,
|
|
197
177
|
fifoQueue,
|
|
@@ -46,7 +46,7 @@ function putWithRetries(table, id, item) {
|
|
|
46
46
|
break;
|
|
47
47
|
}
|
|
48
48
|
let msg = err._0;
|
|
49
|
-
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, `putWithRetries: id=` + id + `: ` + msg), () => ({
|
|
49
|
+
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, undefined, `putWithRetries: id=` + id + `: ` + msg), () => ({
|
|
50
50
|
TAG: "Error",
|
|
51
51
|
_0: `put id=` + id + ` failed: ` + msg
|
|
52
52
|
}));
|
|
@@ -70,7 +70,7 @@ function putIfNotExistsWithRetries(idKey, sortKey, table, id, item) {
|
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
72
72
|
let msg = err._0;
|
|
73
|
-
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, `putIfNotExistsWithRetries: id=` + id + `: ` + msg), () => ({
|
|
73
|
+
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, undefined, `putIfNotExistsWithRetries: id=` + id + `: ` + msg), () => ({
|
|
74
74
|
TAG: "Error",
|
|
75
75
|
_0: `putIfNotExists id=` + id + ` failed: ` + msg
|
|
76
76
|
}));
|
|
@@ -87,7 +87,7 @@ function deleteWithRetries(sort, table, id) {
|
|
|
87
87
|
_0: undefined
|
|
88
88
|
})), DynamoDb_Error$ReventlessAws.retrySchedule), err => {
|
|
89
89
|
let msg = DynamoDb_Error$ReventlessAws.message(err);
|
|
90
|
-
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, `delete: id=` + id + `: ` + msg), () => ({
|
|
90
|
+
return Effect$1.map(EffectLogger$ReventlessCore.logError("Util_DynamoDb_Runtime-ReventlessAws", undefined, undefined, `delete: id=` + id + `: ` + msg), () => ({
|
|
91
91
|
TAG: "Error",
|
|
92
92
|
_0: `delete id=` + id + ` failed: ` + msg
|
|
93
93
|
}));
|
|
@@ -199,7 +199,7 @@ function batchWriteWithRetries(batchWriteRequests) {
|
|
|
199
199
|
}
|
|
200
200
|
let unprocessedRequests = Stdlib_Option.getOrThrow(writeOutput.UnprocessedItems, undefined);
|
|
201
201
|
let count = Object.keys(unprocessedRequests).length.toString();
|
|
202
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_DynamoDb_Runtime-ReventlessAws", undefined, `batchWriteWithRetries: retry ` + retry.toString() + `: ` + count + ` unprocessed items`), () => attempt(retry + 1 | 0, unprocessedRequests));
|
|
202
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_DynamoDb_Runtime-ReventlessAws", undefined, undefined, `batchWriteWithRetries: retry ` + retry.toString() + `: ` + count + ` unprocessed items`), () => attempt(retry + 1 | 0, unprocessedRequests));
|
|
203
203
|
}), err => {
|
|
204
204
|
let msg = DynamoDb_Error$ReventlessAws.message(err);
|
|
205
205
|
return Effect$1.succeed({
|
|
@@ -14,7 +14,7 @@ function putRecord(stream, data) {
|
|
|
14
14
|
PartitionKey: ""
|
|
15
15
|
}))), param => {}), Kinesis_Error$ReventlessAws.retrySchedule), err => {
|
|
16
16
|
let msg = Kinesis_Error$ReventlessAws.message(err);
|
|
17
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_Kinesis_Runtime-ReventlessAws", undefined, `putRecord: ` + msg), () => Effect$1.fail(msg));
|
|
17
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_Kinesis_Runtime-ReventlessAws", undefined, undefined, `putRecord: ` + msg), () => Effect$1.fail(msg));
|
|
18
18
|
}));
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -9,14 +9,14 @@ import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src
|
|
|
9
9
|
function publish(topic, message) {
|
|
10
10
|
return Effect$1.runPromise(Effect$1.catchAll(Effect$1.retry(Effect$1.map(Effect.tryPromise(SNS_Error$ReventlessAws.classify, () => SNS_Helpers$AwsSdk.publish(topic.arn, undefined, message)), param => {}), SNS_Error$ReventlessAws.retrySchedule), err => {
|
|
11
11
|
let msg = SNS_Error$ReventlessAws.message(err);
|
|
12
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SNS_Runtime-ReventlessAws", undefined, "publish: " + msg), () => Effect$1.fail(msg));
|
|
12
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SNS_Runtime-ReventlessAws", undefined, undefined, "publish: " + msg), () => Effect$1.fail(msg));
|
|
13
13
|
}));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function publishFifo(topic, messageGroupId, message) {
|
|
17
17
|
return Effect$1.runPromise(Effect$1.catchAll(Effect$1.retry(Effect$1.map(Effect.tryPromise(SNS_Error$ReventlessAws.classify, () => SNS_Helpers$AwsSdk.publish(topic.arn, messageGroupId, message)), param => {}), SNS_Error$ReventlessAws.retrySchedule), err => {
|
|
18
18
|
let msg = SNS_Error$ReventlessAws.message(err);
|
|
19
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SNS_Runtime-ReventlessAws", undefined, "publishFifo: " + msg), () => Effect$1.fail(msg));
|
|
19
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SNS_Runtime-ReventlessAws", undefined, undefined, "publishFifo: " + msg), () => Effect$1.fail(msg));
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ function send(queue, queueService, commandJson) {
|
|
|
44
44
|
}
|
|
45
45
|
}), () => {}), SQS_Error$ReventlessAws.sendRetrySchedule), err => {
|
|
46
46
|
let msg = SQS_Error$ReventlessAws.message(err);
|
|
47
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, "send: " + msg), () => Effect$1.fail(msg));
|
|
47
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, undefined, "send: " + msg), () => Effect$1.fail(msg));
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -69,7 +69,7 @@ function sendMessages(queue, queueService, commandJsons) {
|
|
|
69
69
|
return failedIds.some(failedId => failedId === msgId);
|
|
70
70
|
});
|
|
71
71
|
if (retry < 5) {
|
|
72
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_SQS_Runtime-ReventlessAws", undefined, `sendMessages: ` + failedIds.length.toString() + ` failed ids, retrying subset`), () => attempt(retry + 1 | 0, commandJsonsToRetry));
|
|
72
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_SQS_Runtime-ReventlessAws", undefined, undefined, `sendMessages: ` + failedIds.length.toString() + ` failed ids, retrying subset`), () => attempt(retry + 1 | 0, commandJsonsToRetry));
|
|
73
73
|
}
|
|
74
74
|
let ids = failedIds.join(", ");
|
|
75
75
|
return Effect$1.fail({
|
|
@@ -79,7 +79,7 @@ function sendMessages(queue, queueService, commandJsons) {
|
|
|
79
79
|
});
|
|
80
80
|
return Effect$1.catchAll(attempt(0, commandJsons), err => {
|
|
81
81
|
let msg = SQS_Error$ReventlessAws.message(err);
|
|
82
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, `sendMessages: ` + msg), () => Effect$1.fail(msg));
|
|
82
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, undefined, `sendMessages: ` + msg), () => Effect$1.fail(msg));
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -104,7 +104,7 @@ function deleteMessages(entries, queue) {
|
|
|
104
104
|
ReceiptHandle: entry.ReceiptHandle
|
|
105
105
|
}));
|
|
106
106
|
if (retry < 5) {
|
|
107
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_SQS_Runtime-ReventlessAws", undefined, `deleteMessages: ` + failedIds.length.toString() + ` failed ids, retrying subset`), () => attempt(retry + 1 | 0, entriesToRetry));
|
|
107
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logInfo("Util_SQS_Runtime-ReventlessAws", undefined, undefined, `deleteMessages: ` + failedIds.length.toString() + ` failed ids, retrying subset`), () => attempt(retry + 1 | 0, entriesToRetry));
|
|
108
108
|
}
|
|
109
109
|
let ids = failedIds.join(", ");
|
|
110
110
|
return Effect$1.fail({
|
|
@@ -114,7 +114,7 @@ function deleteMessages(entries, queue) {
|
|
|
114
114
|
});
|
|
115
115
|
return Effect$1.catchAll(attempt(0, entries), err => {
|
|
116
116
|
let msg = SQS_Error$ReventlessAws.message(err);
|
|
117
|
-
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, `deleteMessages: ` + msg), () => Effect$1.fail(msg));
|
|
117
|
+
return Effect$1.flatMap(EffectLogger$ReventlessCore.logError("Util_SQS_Runtime-ReventlessAws", undefined, undefined, `deleteMessages: ` + msg), () => Effect$1.fail(msg));
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
|