From 2dea83565214fbceb6d8619f339ec249dcfbeae7 Mon Sep 17 00:00:00 2001 From: Marcio Cruz de Almeida <67694075+marciocadev@users.noreply.github.com> Date: Sat, 20 Apr 2024 15:56:54 -0300 Subject: [PATCH] feat(sdk): dead letter queue support for queues (#6060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⛔ As the dead-letter queue linked to the function is a specific case of AWS, I will remove it from the scope of this PR. - [x] dlq (with retries) for queue (tf-aws / awscdk) - [x] dlq (with retries) for queue (sim) ~dlq for function (tf-aws / awscdk)~ ~dlq for function (sim)~ Closes #6033 ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- docs/docs/04-standard-library/cloud/queue.md | 76 +++++ docs/docs/04-standard-library/cloud/topic.md | 14 + .../sdk_tests/queue/dead-letter-queue.test.w | 63 ++++ libs/awscdk/src/queue.ts | 20 +- .../test/__snapshots__/queue.test.ts.snap | 3 + .../incomplete_inflight_namespace.snap | 12 +- .../completions/namespace_middle_dot.snap | 12 +- .../completions/new_expression_nested.snap | 2 +- .../partial_type_reference_annotation.snap | 12 +- .../variable_type_annotation_namespace.snap | 12 +- libs/wingsdk/src/cloud/queue.md | 16 + libs/wingsdk/src/cloud/queue.ts | 27 ++ .../shared-aws/queue.setconsumer.inflight.ts | 12 +- libs/wingsdk/src/target-sim/queue.inflight.ts | 60 +++- .../target-sim/queue.setconsumer.inflight.ts | 11 +- libs/wingsdk/src/target-sim/queue.ts | 21 ++ .../src/target-sim/schema-resources.ts | 9 + libs/wingsdk/src/target-tf-aws/queue.ts | 37 ++- .../__snapshots__/captures.test.ts.snap | 3 + .../__snapshots__/queue.test.ts.snap | 3 + tools/hangar/__snapshots__/platform.ts.snap | 18 + ...dead-letter-queue.test.w_compile_tf-aws.md | 307 ++++++++++++++++++ .../dead-letter-queue.test.w_test_sim.md | 13 + .../queue/queue-ref.test.w_compile_tf-aws.md | 5 +- .../set_consumer.test.w_compile_tf-aws.md | 10 +- .../subscribe-queue.test.w_compile_tf-aws.md | 10 +- .../valid/captures.test.w_compile_tf-aws.md | 5 +- .../file_counter.test.w_compile_tf-aws.md | 5 +- .../valid/hello.test.w_compile_tf-aws.md | 5 +- ...light-subscribers.test.w_compile_tf-aws.md | 5 +- .../valid/redis.test.w_compile_tf-aws.md | 5 +- .../valid/resource.test.w_compile_tf-aws.md | 5 +- .../while_loop_await.test.w_compile_tf-aws.md | 5 +- 33 files changed, 777 insertions(+), 46 deletions(-) create mode 100644 examples/tests/sdk_tests/queue/dead-letter-queue.test.w create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md diff --git a/docs/docs/04-standard-library/cloud/queue.md b/docs/docs/04-standard-library/cloud/queue.md index 646d6f42184..2e9101782ea 100644 --- a/docs/docs/04-standard-library/cloud/queue.md +++ b/docs/docs/04-standard-library/cloud/queue.md @@ -62,6 +62,22 @@ new cloud.Function(inflight () => { }); ``` +### Adding a dead-letter queue + +Creating a queue and adding a dead-letter queue with the maximum number of attempts configured + +```ts playground +bring cloud; + +let dlq = new cloud.Queue() as "dead-letter queue"; +let q = new cloud.Queue( + dlq: { + queue: dlq, + maxDeliveryAttempts: 2 + } +); +``` + ### Referencing an external queue If you would like to reference an existing queue from within your application you can use the @@ -270,6 +286,52 @@ The tree node. ## Structs +### DeadLetterQueueProps + +Dead letter queue options. + +#### Initializer + +```wing +bring cloud; + +let DeadLetterQueueProps = cloud.DeadLetterQueueProps{ ... }; +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| queue | Queue | Queue to receive messages that failed processing. | +| maxDeliveryAttempts | num | Number of times a message will be processed before being sent to the dead-letter queue. | + +--- + +##### `queue`Required + +```wing +queue: Queue; +``` + +- *Type:* Queue + +Queue to receive messages that failed processing. + +--- + +##### `maxDeliveryAttempts`Optional + +```wing +maxDeliveryAttempts: num; +``` + +- *Type:* num +- *Default:* 1 + +Number of times a message will be processed before being sent to the dead-letter queue. + +--- + ### QueueProps Options for `Queue`. @@ -286,11 +348,25 @@ let QueueProps = cloud.QueueProps{ ... }; | **Name** | **Type** | **Description** | | --- | --- | --- | +| dlq | DeadLetterQueueProps | A dead-letter queue. | | retentionPeriod | duration | How long a queue retains a message. | | timeout | duration | How long a queue's consumers have to process a message. | --- +##### `dlq`Optional + +```wing +dlq: DeadLetterQueueProps; +``` + +- *Type:* DeadLetterQueueProps +- *Default:* no dead letter queue + +A dead-letter queue. + +--- + ##### `retentionPeriod`Optional ```wing diff --git a/docs/docs/04-standard-library/cloud/topic.md b/docs/docs/04-standard-library/cloud/topic.md index 3881a60741e..f07374488a7 100644 --- a/docs/docs/04-standard-library/cloud/topic.md +++ b/docs/docs/04-standard-library/cloud/topic.md @@ -395,11 +395,25 @@ let TopicSubscribeQueueOptions = cloud.TopicSubscribeQueueOptions{ ... }; | **Name** | **Type** | **Description** | | --- | --- | --- | +| dlq | DeadLetterQueueProps | A dead-letter queue. | | retentionPeriod | duration | How long a queue retains a message. | | timeout | duration | How long a queue's consumers have to process a message. | --- +##### `dlq`Optional + +```wing +dlq: DeadLetterQueueProps; +``` + +- *Type:* DeadLetterQueueProps +- *Default:* no dead letter queue + +A dead-letter queue. + +--- + ##### `retentionPeriod`Optional ```wing diff --git a/examples/tests/sdk_tests/queue/dead-letter-queue.test.w b/examples/tests/sdk_tests/queue/dead-letter-queue.test.w new file mode 100644 index 00000000000..a75c0587426 --- /dev/null +++ b/examples/tests/sdk_tests/queue/dead-letter-queue.test.w @@ -0,0 +1,63 @@ +bring cloud; +bring util; + +let counter_received_messages = new cloud.Counter(); + +let dlq_without_retries = new cloud.Queue() as "dlq without retries"; +let queue_without_retries = new cloud.Queue( + dlq: { queue: dlq_without_retries } +) as "queue without retries"; +queue_without_retries.setConsumer(inflight (msg: str) => { + counter_received_messages.inc(1, msg); + if msg == "fail" { + throw "error"; + } +}); + + +new std.Test( + inflight () => { + queue_without_retries.push("Hello"); + queue_without_retries.push("fail"); + queue_without_retries.push("World!"); + + // wait until it executes once. + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("Hello") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("World!") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("fail") == 1; })); + + // check if the "fail" message has arrived at the dead-letter queue + assert(util.waitUntil(inflight () => { return dlq_without_retries.pop() == "fail"; })); + }, + // To make this test work on AWS, it's necessary to set a high timeout + // because if the message fails, we have to wait for the visibility timeout + // to expire in order to retrieve the same message from the queue again. + timeout: 5m) as "one execution and send fail message to dead-letter queue"; + +let dlq_with_retries = new cloud.Queue() as "dlq with retries"; +let queue_with_retries = new cloud.Queue( + dlq: { + queue: dlq_with_retries, + maxDeliveryAttempts: 2 + } +) as "queue with retries"; +queue_with_retries.setConsumer(inflight (msg: str) => { + counter_received_messages.inc(1, msg); + if msg == "fail" { + throw "error"; + } +}); + +new std.Test(inflight () => { + queue_with_retries.push("Hello"); + queue_with_retries.push("fail"); + queue_with_retries.push("World!"); + + // wait until it executes once and retry one more times. + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("Hello") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("World!") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("fail") == 2; })); + + // check if the "fail" message has arrived at the dead-letter queue + assert(util.waitUntil(inflight () => { return dlq_with_retries.pop() == "fail"; })); +}, timeout: 5m) as "one execution, two retries and send the fail message to dead-letter queue"; diff --git a/libs/awscdk/src/queue.ts b/libs/awscdk/src/queue.ts index 248fe7b3ca2..4ea5a0887fe 100644 --- a/libs/awscdk/src/queue.ts +++ b/libs/awscdk/src/queue.ts @@ -7,7 +7,7 @@ import { App } from "./app"; import { std, core, cloud } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { calculateQueuePermissions } from "@winglang/sdk/lib/shared-aws/permissions"; -import { IAwsQueue } from "@winglang/sdk/lib/shared-aws/queue"; +import { IAwsQueue, Queue as AwsQueue } from "@winglang/sdk/lib/shared-aws/queue"; import { addPolicyStatements, isAwsCdkFunction } from "./function"; /** @@ -23,14 +23,27 @@ export class Queue extends cloud.Queue implements IAwsQueue { super(scope, id, props); this.timeout = props.timeout ?? std.Duration.fromSeconds(30); - this.queue = new SQSQueue(this, "Default", { + const queueOpt = props.dlq ? { visibilityTimeout: props.timeout ? Duration.seconds(props.timeout?.seconds) : Duration.seconds(30), retentionPeriod: props.retentionPeriod ? Duration.seconds(props.retentionPeriod?.seconds) : Duration.hours(1), - }); + deadLetterQueue: { + queue: SQSQueue.fromQueueArn(this, "DeadLetterQueue", AwsQueue.from(props.dlq.queue)?.queueArn!), + maxReceiveCount: props.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + } + } : { + visibilityTimeout: props.timeout + ? Duration.seconds(props.timeout?.seconds) + : Duration.seconds(30), + retentionPeriod: props.retentionPeriod + ? Duration.seconds(props.retentionPeriod?.seconds) + : Duration.hours(1), + } + + this.queue = new SQSQueue(this, "Default", queueOpt); } public setConsumer( @@ -63,6 +76,7 @@ export class Queue extends cloud.Queue implements IAwsQueue { const eventSource = new SqsEventSource(this.queue, { batchSize: props.batchSize ?? 1, + reportBatchItemFailures: true, }); fn.awscdkFunction.addEventSource(eventSource); diff --git a/libs/awscdk/test/__snapshots__/queue.test.ts.snap b/libs/awscdk/test/__snapshots__/queue.test.ts.snap index fce461496c1..ff7d8c6f540 100644 --- a/libs/awscdk/test/__snapshots__/queue.test.ts.snap +++ b/libs/awscdk/test/__snapshots__/queue.test.ts.snap @@ -192,6 +192,9 @@ exports[`queue with a consumer function 2`] = ` "FunctionName": { "Ref": "QueueSetConsumer06749388A", }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures", + ], }, "Type": "AWS::Lambda::EventSourceMapping", }, diff --git a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap +++ b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap index 396726602d3..bfe3e0f0bf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap +++ b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap @@ -82,7 +82,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue insertText: Queue($1) insertTextFormat: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap +++ b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingsdk/src/cloud/queue.md b/libs/wingsdk/src/cloud/queue.md index 9e769410044..04c656ccffb 100644 --- a/libs/wingsdk/src/cloud/queue.md +++ b/libs/wingsdk/src/cloud/queue.md @@ -62,6 +62,22 @@ new cloud.Function(inflight () => { }); ``` +### Adding a dead-letter queue + +Creating a queue and adding a dead-letter queue with the maximum number of attempts configured + +```ts playground +bring cloud; + +let dlq = new cloud.Queue() as "dead-letter queue"; +let q = new cloud.Queue( + dlq: { + queue: dlq, + maxDeliveryAttempts: 2 + } +); +``` + ### Referencing an external queue If you would like to reference an existing queue from within your application you can use the diff --git a/libs/wingsdk/src/cloud/queue.ts b/libs/wingsdk/src/cloud/queue.ts index 00fc8740c8b..2380d2a7331 100644 --- a/libs/wingsdk/src/cloud/queue.ts +++ b/libs/wingsdk/src/cloud/queue.ts @@ -10,6 +10,27 @@ import { Duration, IInflight, Node, Resource } from "../std"; */ export const QUEUE_FQN = fqnForType("cloud.Queue"); +/** + * Dead-letter queue default retries + */ +export const DEFAULT_DELIVERY_ATTEMPTS = 1; + +/** + * Dead letter queue options. + */ +export interface DeadLetterQueueProps { + /** + * Queue to receive messages that failed processing. + */ + readonly queue: Queue; + /** + * Number of times a message will be processed before being + * sent to the dead-letter queue. + * @default 1 + */ + readonly maxDeliveryAttempts?: number; +} + /** * Options for `Queue`. */ @@ -25,6 +46,12 @@ export interface QueueProps { * @default 1h */ readonly retentionPeriod?: Duration; + + /** + * A dead-letter queue. + * @default - no dead letter queue + */ + readonly dlq?: DeadLetterQueueProps; } /** diff --git a/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts b/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts index 2a192a76149..b9b35caaec0 100644 --- a/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts +++ b/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts @@ -10,9 +10,17 @@ export class QueueSetConsumerHandlerClient constructor({ handler }: { handler: IFunctionHandlerClient }) { this.handler = handler; } - public async handle(event: any) { + public async handle(event: any): Promise { + const batchItemFailures = []; for (const record of event.Records ?? []) { - await this.handler.handle(record.body); + try { + await this.handler.handle(record.body); + } catch (error) { + batchItemFailures.push({ + itemIdentifier: record.messageId, + }); + } } + return { batchItemFailures }; } } diff --git a/libs/wingsdk/src/target-sim/queue.inflight.ts b/libs/wingsdk/src/target-sim/queue.inflight.ts index ade75198fb6..8700f5ba0b9 100644 --- a/libs/wingsdk/src/target-sim/queue.inflight.ts +++ b/libs/wingsdk/src/target-sim/queue.inflight.ts @@ -5,9 +5,15 @@ import { QueueSchema, QueueSubscriber, EventSubscription, + DeadLetterQueueSchema, ResourceHandle, } from "./schema-resources"; -import { IFunctionClient, IQueueClient, QUEUE_FQN } from "../cloud"; +import { + DEFAULT_DELIVERY_ATTEMPTS, + IFunctionClient, + IQueueClient, + QUEUE_FQN, +} from "../cloud"; import { ISimulatorContext, ISimulatorResourceInstance, @@ -24,10 +30,12 @@ export class Queue private _context: ISimulatorContext | undefined; private readonly timeoutSeconds: number; private readonly retentionPeriod: number; + private readonly dlq?: DeadLetterQueueSchema; constructor(props: QueueSchema) { this.timeoutSeconds = props.timeout; this.retentionPeriod = props.retentionPeriod; + this.dlq = props.dlq; this.processLoop = runEvery(100, async () => this.processMessages()); // every 0.1 seconds } @@ -85,7 +93,13 @@ export class Queue throw new Error("Empty messages are not allowed"); } for (const message of messages) { - this.messages.push(new QueueMessage(this.retentionPeriod, message)); + this.messages.push( + new QueueMessage( + this.retentionPeriod, + DEFAULT_DELIVERY_ATTEMPTS, + message + ) + ); } }, }); @@ -187,8 +201,38 @@ export class Queue // we don't use invokeAsync here because we want to wait for the function to finish // and requeue the messages if it fails - void fnClient - .invoke(JSON.stringify({ messages: messagesPayload })) + await fnClient + .invoke(JSON.stringify({ messages: messages })) + .then((result) => { + if (this.dlq && result) { + const errorList = JSON.parse(result); + let retriesMessages = []; + for (const msg of errorList) { + if ( + msg.remainingDeliveryAttempts < this.dlq.maxDeliveryAttempts + ) { + msg.remainingDeliveryAttempts++; + retriesMessages.push(msg); + } else { + let dlq = this.context.getClient( + this.dlq.dlqHandler + ) as IQueueClient; + void dlq.push(msg.payload).catch((err) => { + this.context.addTrace({ + type: TraceType.RESOURCE, + data: { + message: `Pushing messages to the dead-letter queue generates an error -> ${err}`, + }, + sourcePath: this.context.resourcePath, + sourceType: QUEUE_FQN, + timestamp: new Date().toISOString(), + }); + }); + } + } + this.messages.push(...retriesMessages); + } + }) .catch((err) => { // If the function is at a concurrency limit, pretend we just didn't call it if ( @@ -238,12 +282,18 @@ export class Queue class QueueMessage { public readonly retentionTimeout: Date; public readonly payload: string; + public remainingDeliveryAttempts: number; - constructor(retentionPeriod: number, message: string) { + constructor( + retentionPeriod: number, + remainingDeliveryAttempts: number, + message: string + ) { const currentTime = new Date(); currentTime.setSeconds(retentionPeriod + currentTime.getSeconds()); this.retentionTimeout = currentTime; this.payload = message; + this.remainingDeliveryAttempts = remainingDeliveryAttempts; } } diff --git a/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts b/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts index 55dad14f8b0..db2b90ca570 100644 --- a/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts +++ b/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts @@ -9,11 +9,18 @@ export class QueueSetConsumerHandlerClient implements IFunctionHandlerClient { this.handler = handler; } public async handle(event?: string) { + const batchItemFailures = []; let parsed = JSON.parse(event ?? "{}"); if (!parsed.messages) throw new Error('No "messages" field in event.'); for (const $message of parsed.messages) { - await this.handler.handle($message); + try { + await this.handler.handle($message.payload); + } catch (error) { + batchItemFailures.push($message); + } } - return undefined; + return batchItemFailures.length > 0 + ? JSON.stringify(batchItemFailures) + : undefined; } } diff --git a/libs/wingsdk/src/target-sim/queue.ts b/libs/wingsdk/src/target-sim/queue.ts index 006482ad3ac..30d5d661b2b 100644 --- a/libs/wingsdk/src/target-sim/queue.ts +++ b/libs/wingsdk/src/target-sim/queue.ts @@ -9,6 +9,7 @@ import { import { Policy } from "./policy"; import { ISimulatorResource } from "./resource"; import { QueueSchema } from "./schema-resources"; +import { simulatorHandleToken } from "./tokens"; import { bindSimulatorResource, makeSimulatorJsClient } from "./util"; import * as cloud from "../cloud"; import { NotImplementedError } from "../core/errors"; @@ -24,6 +25,7 @@ import { Duration, IInflightHost, Node, SDK_SOURCE_MODULE } from "../std"; export class Queue extends cloud.Queue implements ISimulatorResource { private readonly timeout: Duration; private readonly retentionPeriod: Duration; + private readonly dlq?: cloud.DeadLetterQueueProps; private readonly policy: Policy; constructor(scope: Construct, id: string, props: cloud.QueueProps = {}) { @@ -50,6 +52,18 @@ export class Queue extends cloud.Queue implements ISimulatorResource { } this.policy = new Policy(this, "Policy", { principal: this }); + + if (props.dlq && props.dlq.queue) { + this.dlq = props.dlq; + + this.policy.addStatement(this.dlq.queue, cloud.QueueInflightMethods.PUSH); + + Node.of(this).addConnection({ + source: this, + target: this.dlq.queue, + name: "dead-letter queue", + }); + } } /** @internal */ @@ -136,6 +150,13 @@ export class Queue extends cloud.Queue implements ISimulatorResource { const props: QueueSchema = { timeout: this.timeout.seconds, retentionPeriod: this.retentionPeriod.seconds, + dlq: this.dlq + ? { + dlqHandler: simulatorHandleToken(this.dlq.queue), + maxDeliveryAttempts: + this.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + } + : undefined, }; return { type: cloud.QUEUE_FQN, diff --git a/libs/wingsdk/src/target-sim/schema-resources.ts b/libs/wingsdk/src/target-sim/schema-resources.ts index 91a56fc763f..a482bbeb451 100644 --- a/libs/wingsdk/src/target-sim/schema-resources.ts +++ b/libs/wingsdk/src/target-sim/schema-resources.ts @@ -48,12 +48,21 @@ export interface FunctionSchema { /** Runtime attributes for cloud.Function */ export interface FunctionAttributes {} +export interface DeadLetterQueueSchema { + /** Dead-letter queue handler token */ + dlqHandler: string; + /** Number of time a message will be processed */ + maxDeliveryAttempts: number; +} + /** Schema for cloud.Queue */ export interface QueueSchema { /** How long a queue's consumers have to process a message, in seconds */ readonly timeout: number; /** How long a queue retains a message, in seconds */ readonly retentionPeriod: number; + /** Dead-letter queue options */ + readonly dlq?: DeadLetterQueueSchema; } /** Runtime attributes for cloud.Queue */ diff --git a/libs/wingsdk/src/target-tf-aws/queue.ts b/libs/wingsdk/src/target-tf-aws/queue.ts index 988ee0ac540..6a38a525e5b 100644 --- a/libs/wingsdk/src/target-tf-aws/queue.ts +++ b/libs/wingsdk/src/target-tf-aws/queue.ts @@ -10,6 +10,7 @@ import { convertBetweenHandlers } from "../shared/convert"; import { NameOptions, ResourceNames } from "../shared/resource-names"; import { IAwsQueue } from "../shared-aws"; import { calculateQueuePermissions } from "../shared-aws/permissions"; +import { Queue as AwsQueue } from "../shared-aws/queue"; import { Duration, IInflightHost, Node } from "../std"; /** @@ -32,15 +33,32 @@ export class Queue extends cloud.Queue implements IAwsQueue { constructor(scope: Construct, id: string, props: cloud.QueueProps = {}) { super(scope, id, props); - this.queue = new SqsQueue(this, "Default", { - visibilityTimeoutSeconds: props.timeout - ? props.timeout.seconds - : Duration.fromSeconds(30).seconds, - messageRetentionSeconds: props.retentionPeriod - ? props.retentionPeriod.seconds - : Duration.fromHours(1).seconds, - name: ResourceNames.generateName(this, NAME_OPTS), - }); + const queueOpt = props.dlq + ? { + visibilityTimeoutSeconds: props.timeout + ? props.timeout.seconds + : Duration.fromSeconds(30).seconds, + messageRetentionSeconds: props.retentionPeriod + ? props.retentionPeriod.seconds + : Duration.fromHours(1).seconds, + name: ResourceNames.generateName(this, NAME_OPTS), + redrivePolicy: JSON.stringify({ + deadLetterTargetArn: AwsQueue.from(props.dlq.queue)?.queueArn, + maxReceiveCount: + props.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + }), + } + : { + visibilityTimeoutSeconds: props.timeout + ? props.timeout.seconds + : Duration.fromSeconds(30).seconds, + messageRetentionSeconds: props.retentionPeriod + ? props.retentionPeriod.seconds + : Duration.fromHours(1).seconds, + name: ResourceNames.generateName(this, NAME_OPTS), + }; + + this.queue = new SqsQueue(this, "Default", queueOpt); } /** @internal */ @@ -99,6 +117,7 @@ export class Queue extends cloud.Queue implements IAwsQueue { functionName: fn.functionName, eventSourceArn: this.queue.arn, batchSize: props.batchSize ?? 1, + functionResponseTypes: ["ReportBatchItemFailures"], // It allows the function to return the messages that failed to the queue }); Node.of(this).addConnection({ diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap index ecf8f57cca0..065e1c16ea2 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap @@ -222,6 +222,9 @@ exports[`function with a queue binding 3`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap index 28e841f6eb5..bf2f4ed0b91 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap @@ -831,6 +831,9 @@ exports[`queue with a consumer function 2`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/platform.ts.snap b/tools/hangar/__snapshots__/platform.ts.snap index 61e7c5e154e..9c3e6eefe06 100644 --- a/tools/hangar/__snapshots__/platform.ts.snap +++ b/tools/hangar/__snapshots__/platform.ts.snap @@ -74,6 +74,9 @@ exports[`Multiple platforms > only first platform app is used 1`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -236,6 +239,9 @@ exports[`Platform examples > AWS target platform > permission-boundary.js 1`] = "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -467,6 +473,9 @@ exports[`Platform examples > AWS target platform > replicate-s3.js 1`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -746,6 +755,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > azurerm backe "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -907,6 +919,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > gcp backend 1 "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -1068,6 +1083,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > s3 backend 1` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..74d09daa612 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md @@ -0,0 +1,307 @@ +# [dead-letter-queue.test.w](../../../../../../examples/tests/sdk_tests/queue/dead-letter-queue.test.w) | compile | tf-aws + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": {} + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_cloudwatch_log_group": { + "queuewithoutretries-SetConsumer0_CloudwatchLogGroup_9265B40C": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "queuewithoutretries-SetConsumer0_CloudwatchLogGroup_9265B40C" + } + }, + "name": "/aws/lambda/queue-without-retries-SetConsumer0-c8ba2958", + "retention_in_days": 30 + }, + "queuewithretries-SetConsumer0_CloudwatchLogGroup_98D9A088": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "queuewithretries-SetConsumer0_CloudwatchLogGroup_98D9A088" + } + }, + "name": "/aws/lambda/queue-with-retries-SetConsumer0-c82b1a26", + "retention_in_days": 30 + } + }, + "aws_dynamodb_table": { + "Counter": { + "//": { + "metadata": { + "path": "root/Default/Default/Counter/Default", + "uniqueId": "Counter" + } + }, + "attribute": [ + { + "name": "id", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "hash_key": "id", + "name": "wing-counter-Counter-c824ef62" + } + }, + "aws_iam_role": { + "queuewithoutretries-SetConsumer0_IamRole_F76F585E": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRole", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRole_F76F585E" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "queuewithretries-SetConsumer0_IamRole_B95C0A26": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRole", + "uniqueId": "queuewithretries-SetConsumer0_IamRole_B95C0A26" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + } + }, + "aws_iam_role_policy": { + "queuewithoutretries-SetConsumer0_IamRolePolicy_2603CFA0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRolePolicy", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRolePolicy_2603CFA0" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.queuewithoutretries.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.Counter.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.name}" + }, + "queuewithretries-SetConsumer0_IamRolePolicy_245846E1": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRolePolicy", + "uniqueId": "queuewithretries-SetConsumer0_IamRolePolicy_245846E1" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.queuewithretries.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.Counter.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.name}" + } + }, + "aws_iam_role_policy_attachment": { + "queuewithoutretries-SetConsumer0_IamRolePolicyAttachment_559EECE4": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRolePolicyAttachment_559EECE4" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.name}" + }, + "queuewithretries-SetConsumer0_IamRolePolicyAttachment_831C8294": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "queuewithretries-SetConsumer0_IamRolePolicyAttachment_831C8294" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.name}" + } + }, + "aws_lambda_event_source_mapping": { + "queuewithoutretries_EventSourceMapping_963C2B4C": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries/EventSourceMapping", + "uniqueId": "queuewithoutretries_EventSourceMapping_963C2B4C" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.queuewithoutretries.arn}", + "function_name": "${aws_lambda_function.queuewithoutretries-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] + }, + "queuewithretries_EventSourceMapping_A0EC80F3": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries/EventSourceMapping", + "uniqueId": "queuewithretries_EventSourceMapping_A0EC80F3" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.queuewithretries.arn}", + "function_name": "${aws_lambda_function.queuewithretries-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] + } + }, + "aws_lambda_function": { + "queuewithoutretries-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/Default", + "uniqueId": "queuewithoutretries-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_6cb5a3a4": "${aws_dynamodb_table.Counter.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "queue-without-retries-SetConsumer0-c8ba2958", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "queue-without-retries-SetConsumer0-c8ba2958", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.queuewithoutretries-SetConsumer0_S3Object_ECD1395D.key}", + "timeout": "${aws_sqs_queue.queuewithoutretries.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "queuewithretries-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/Default", + "uniqueId": "queuewithretries-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_6cb5a3a4": "${aws_dynamodb_table.Counter.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "queue-with-retries-SetConsumer0-c82b1a26", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "queue-with-retries-SetConsumer0-c82b1a26", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.queuewithretries-SetConsumer0_S3Object_29ED03EE.key}", + "timeout": "${aws_sqs_queue.queuewithretries.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + } + }, + "aws_s3_bucket": { + "Code": { + "//": { + "metadata": { + "path": "root/Default/Code", + "uniqueId": "Code" + } + }, + "bucket_prefix": "code-c84a50b1-" + } + }, + "aws_s3_object": { + "queuewithoutretries-SetConsumer0_S3Object_ECD1395D": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/S3Object", + "uniqueId": "queuewithoutretries-SetConsumer0_S3Object_ECD1395D" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "queuewithretries-SetConsumer0_S3Object_29ED03EE": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/S3Object", + "uniqueId": "queuewithretries-SetConsumer0_S3Object_29ED03EE" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + } + }, + "aws_sqs_queue": { + "dlqwithoutretries": { + "//": { + "metadata": { + "path": "root/Default/Default/dlq without retries/Default", + "uniqueId": "dlqwithoutretries" + } + }, + "message_retention_seconds": 3600, + "name": "dlq-without-retries-c83c0330", + "visibility_timeout_seconds": 30 + }, + "dlqwithretries": { + "//": { + "metadata": { + "path": "root/Default/Default/dlq with retries/Default", + "uniqueId": "dlqwithretries" + } + }, + "message_retention_seconds": 3600, + "name": "dlq-with-retries-c877f5c7", + "visibility_timeout_seconds": 30 + }, + "queuewithoutretries": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries/Default", + "uniqueId": "queuewithoutretries" + } + }, + "message_retention_seconds": 3600, + "name": "queue-without-retries-c8a5001c", + "redrive_policy": "{\"deadLetterTargetArn\":\"${aws_sqs_queue.dlqwithoutretries.arn}\",\"maxReceiveCount\":1}", + "visibility_timeout_seconds": 30 + }, + "queuewithretries": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries/Default", + "uniqueId": "queuewithretries" + } + }, + "message_retention_seconds": 3600, + "name": "queue-with-retries-c8a06dc7", + "redrive_policy": "{\"deadLetterTargetArn\":\"${aws_sqs_queue.dlqwithretries.arn}\",\"maxReceiveCount\":2}", + "visibility_timeout_seconds": 30 + } + } + } +} +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md new file mode 100644 index 00000000000..f35d5d2ed2d --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md @@ -0,0 +1,13 @@ +# [dead-letter-queue.test.w](../../../../../../examples/tests/sdk_tests/queue/dead-letter-queue.test.w) | test | sim + +## stdout.log +```log +pass ─ dead-letter-queue.test.wsim » root/env0/one execution and send fail message to dead-letter queue +pass ─ dead-letter-queue.test.wsim » root/env1/one execution, two retries and send the fail message to dead-letter queue + +Tests 2 passed (2) +Snapshots 1 skipped +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md index 5559abc4cb1..598c74b34e6 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md @@ -249,7 +249,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md index 6fea628fc06..e1da8b967b6 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md @@ -149,7 +149,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] }, "q2_EventSourceMapping_F484014F": { "//": { @@ -160,7 +163,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q2.arn}", - "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md index 8a0674fd361..f833a206df1 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md @@ -171,7 +171,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q1.arn}", - "function_name": "${aws_lambda_function.q1-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q1-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] }, "q2_EventSourceMapping_F484014F": { "//": { @@ -182,7 +185,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q2.arn}", - "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md index 746445c0c10..01ee869a182 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md @@ -349,7 +349,10 @@ module.exports = function({ $headers }) { }, "batch_size": 5, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md index bd433ead881..7cab4a39fc8 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md @@ -115,7 +115,10 @@ module.exports = function({ $bucket, $counter }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md index a89822806fa..27941b931ba 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md @@ -94,7 +94,10 @@ module.exports = function({ $bucket }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md index ae14796da53..32e4f4a90e5 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md @@ -153,7 +153,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md index fdc84d01919..8159814e3fa 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md @@ -223,7 +223,10 @@ module.exports = function({ $queue, $r, $r2, $util_Util }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md index f87bdd61606..c43fc4d5a66 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md @@ -397,7 +397,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.BigPublisher_Queue_2C024F97.arn}", - "function_name": "${aws_lambda_function.BigPublisher_Queue-SetConsumer0_55896C65.function_name}" + "function_name": "${aws_lambda_function.BigPublisher_Queue-SetConsumer0_55896C65.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md index c73bf15bf1c..5baaa695c11 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md @@ -100,7 +100,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": {