@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +9 -9
  3. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +4 -1
  4. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +1 -1
  5. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +4 -1
  6. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +1 -1
  7. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +4 -1
  8. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +1 -1
  9. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs +3 -3
  10. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +4 -1
  11. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +1 -1
  12. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +4 -1
  13. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +1 -1
  14. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +4 -1
  15. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +1 -1
  16. package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs +2 -2
  17. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs +1 -1
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +4 -1
  19. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -1
  20. package/src/adapter/Postgres/PgProjectionFeed.res +4 -1
  21. package/src/adapter/Postgres/PgProjectionFeed.res.mjs +1 -1
  22. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +12 -0
  23. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +4 -1
  24. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs +14 -14
  25. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +4 -4
  26. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -5
  27. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +3 -3
  28. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +2 -2
  29. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +1 -1
  30. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +18 -1
  31. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +20 -6
  32. package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs +1 -1
  33. package/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs +1 -1
  34. package/src/util/Util_CognitoGroupUser_Runtime.res.mjs +2 -2
  35. package/src/util/Util_Cognito_Runtime.res.mjs +1 -1
  36. package/src/util/Util_DeadLetterHandler.res +138 -0
  37. package/src/util/Util_DeadLetterHandler.res.mjs +113 -0
  38. package/src/util/Util_DeadLetterQueue.res +10 -53
  39. package/src/util/Util_DeadLetterQueue.res.mjs +6 -26
  40. package/src/util/Util_DynamoDb_Runtime.res.mjs +4 -4
  41. package/src/util/Util_Kinesis_Runtime.res.mjs +1 -1
  42. package/src/util/Util_SNS_Runtime.res.mjs +2 -2
  43. package/src/util/Util_SQS_Runtime.res.mjs +5 -5
  44. package/tests/Util_DeadLetterHandlerTest.res +78 -0
  45. 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
- if (pgConnection !== undefined) {
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 */
@@ -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
- // Logs what arrived, then FAILS the invocation on purpose.
71
- //
72
- // Returning success here would let SQS delete the message, which is what this
73
- // handler used to do — and it made a dead letter unobservable twice over: the
74
- // queue's depth returned to 0, so a depth alarm could never fire, and the
75
- // function's `Errors` metric stayed at 0, so neither could an error alarm. A
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 = `export const handler = async (event) => {
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