async-lambda-unstable 0.3.2__tar.gz → 0.3.5__tar.gz

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 (29) hide show
  1. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/PKG-INFO +60 -2
  2. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/README.md +59 -1
  3. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/__init__.py +1 -1
  4. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/build_config.py +13 -1
  5. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/controller.py +98 -7
  6. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_event.py +4 -2
  7. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/task.py +126 -51
  8. async-lambda-unstable-0.3.5/async_lambda/util.py +8 -0
  9. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/PKG-INFO +60 -2
  10. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/SOURCES.txt +1 -0
  11. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/cli.py +0 -0
  12. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/client.py +0 -0
  13. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/config.py +0 -0
  14. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/env.py +0 -0
  15. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/__init__.py +0 -0
  16. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/__init__.py +0 -0
  17. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/api_event.py +0 -0
  18. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/base_event.py +0 -0
  19. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/managed_sqs_event.py +0 -0
  20. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/scheduled_event.py +0 -0
  21. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/unmanaged_sqs_event.py +0 -0
  22. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_context.py +0 -0
  23. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/py.typed +0 -0
  24. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/dependency_links.txt +0 -0
  25. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/entry_points.txt +0 -0
  26. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/requires.txt +0 -0
  27. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/top_level.txt +0 -0
  28. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/pyproject.toml +0 -0
  29. {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-lambda-unstable
3
- Version: 0.3.2
3
+ Version: 0.3.5
4
4
  Summary: A framework for creating AWS Lambda Async Workflows. - Unstable Branch
5
5
  Author-email: "Nuclei, Inc" <engineering@nuclei.ai>
6
6
  Description-Content-Type: text/markdown
@@ -47,7 +47,7 @@ All task decorators share common arguments for configuring the underlying lambda
47
47
  - `memory: int = 128` Sets the memory allocation for the function.
48
48
  - `timeout: int = 60` Sets the timeout for the function (max 900 seconds).
49
49
  - `ephemeral_storage: int = 512` Sets the ephemeral storage allocation for the function.
50
- - `maximum_concurrency: Optional[int] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.)
50
+ - `maximum_concurrency: Optional[int | List[int]] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.) When using the `lanes` feature, this can be a list of maximum concurrency for each lane. The length of the list must equal the # of lanes.
51
51
 
52
52
  ## Async Task
53
53
 
@@ -82,6 +82,34 @@ def task_1(event: ManagedSQSEvent):
82
82
  app.async_invoke("Task1", {})
83
83
  ```
84
84
 
85
+ ### Lanes
86
+
87
+ Sometimes you may want multiple "lanes" for events to travel through, especially when you have constrained throughput with `maximum_concurrency`. Utilize the `lanes` feature to open up multiple paths to an `async-task`. This can be useful if you have a large backlog of messages you need to process, but you don't want to interrupt the normal message flow.
88
+
89
+ The # of lanes can be controlled at the controller, sub-controller, and/or task level. With the configuration propagating down the tree, but it can be overridden at any of the levels. The # of lanes can be set with the `lane_count` parameter.
90
+
91
+ By default all usages of `async_invoke` will place the message in the default lane (`0`). To change this specify `lane=` in the `async_invoke` call. By default, any further calls of `async_invoke` down the call stack will continue to put the messages into the same lane if it is available. You can turn of this behavior by setting `propagate_lane_assignment=False` at the controller level.
92
+
93
+ For example, we will use a payload field to determine which lane processing should occur in. We will set the maximum concurrency for the default lane at 10, and for the other lane at `2`.
94
+
95
+ ```python
96
+ app = AsyncLambdaController(lane_count=2)
97
+
98
+ @app.async_task("SwitchBoard")
99
+ def switch_board(event: ManagedSQSEvent):
100
+ value = event.payload['value']
101
+ lane = 0
102
+ if value > 50_000:
103
+ lane = 1
104
+ app.async_invoke("ProcessingTask", event.payload, lane=lane)
105
+
106
+ @app.async_task("ProcessingTask", maximum_concurrency=[10, 2])
107
+ def processing_task(event: ManagedSQSEvent):
108
+ ...
109
+ ```
110
+
111
+ `async-lambda` creates `n` queues and lambda triggers per `async-task` where `n = lane_count`. All of the `n` queues are still consumed by a single lambda function.
112
+
85
113
  ## Unmanaged SQS Task
86
114
 
87
115
  Unmanaged SQS tasks consume from any arbitrary SQS queue (1 message per invocation).
@@ -243,6 +271,20 @@ so that you can reference the extras and the associated managed sqs resource by
243
271
  - `$QUEUEID"` will be replaced with the `LogicalId` of the associated Managed SQS queue.
244
272
  - `$EXTRA<index>` will be replaced with the `LogicalId` of the extra at the specified index.
245
273
 
274
+ ## `method_settings`
275
+
276
+ **This config value can only be set at the app or stage level.**
277
+
278
+ ```
279
+ [
280
+ {...}
281
+ ]
282
+ ```
283
+
284
+ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverless::Api` resource is created.
285
+
286
+ The value is passed into the [`MethodSettings`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-methodsettings) property of the `AWS::Serverless::Api`. The spec for `MethodSetting` can be found [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html).
287
+
246
288
  ## `domain_name`
247
289
 
248
290
  **This config value can only be set at the app or stage level.**
@@ -281,6 +323,22 @@ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverles
281
323
 
282
324
  This config value will set the [`CertificateArn`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-api-domainconfiguration.html) field of the [`Domain` property](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-domain)
283
325
 
326
+ ## `tags`
327
+
328
+ ```
329
+ {
330
+ "TAG_NAME": "TAG_VALUE"
331
+ }
332
+ ```
333
+
334
+ This config value will set the `Tags` field of all resources created by async-lambda. This will not set the field on `managed_queue_extras` resources.
335
+
336
+ The keys `framework` and `framework-version` will always be set and the system values will override any values set by the user.
337
+
338
+ For managed queues the tags `async-lambda-queue-type` will be set to `dlq`, `dlq-task`, or `managed` depending on the queue type.
339
+
340
+ For `async_task` queues (non dlq-task) the `async-lambda-lane` will be set.
341
+
284
342
  # Building an `async-lambda` app
285
343
 
286
344
  **When the app is packaged for lambda, only the main module, and the `vendor` and `src` directories are included.**
@@ -39,7 +39,7 @@ All task decorators share common arguments for configuring the underlying lambda
39
39
  - `memory: int = 128` Sets the memory allocation for the function.
40
40
  - `timeout: int = 60` Sets the timeout for the function (max 900 seconds).
41
41
  - `ephemeral_storage: int = 512` Sets the ephemeral storage allocation for the function.
42
- - `maximum_concurrency: Optional[int] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.)
42
+ - `maximum_concurrency: Optional[int | List[int]] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.) When using the `lanes` feature, this can be a list of maximum concurrency for each lane. The length of the list must equal the # of lanes.
43
43
 
44
44
  ## Async Task
45
45
 
@@ -74,6 +74,34 @@ def task_1(event: ManagedSQSEvent):
74
74
  app.async_invoke("Task1", {})
75
75
  ```
76
76
 
77
+ ### Lanes
78
+
79
+ Sometimes you may want multiple "lanes" for events to travel through, especially when you have constrained throughput with `maximum_concurrency`. Utilize the `lanes` feature to open up multiple paths to an `async-task`. This can be useful if you have a large backlog of messages you need to process, but you don't want to interrupt the normal message flow.
80
+
81
+ The # of lanes can be controlled at the controller, sub-controller, and/or task level. With the configuration propagating down the tree, but it can be overridden at any of the levels. The # of lanes can be set with the `lane_count` parameter.
82
+
83
+ By default all usages of `async_invoke` will place the message in the default lane (`0`). To change this specify `lane=` in the `async_invoke` call. By default, any further calls of `async_invoke` down the call stack will continue to put the messages into the same lane if it is available. You can turn of this behavior by setting `propagate_lane_assignment=False` at the controller level.
84
+
85
+ For example, we will use a payload field to determine which lane processing should occur in. We will set the maximum concurrency for the default lane at 10, and for the other lane at `2`.
86
+
87
+ ```python
88
+ app = AsyncLambdaController(lane_count=2)
89
+
90
+ @app.async_task("SwitchBoard")
91
+ def switch_board(event: ManagedSQSEvent):
92
+ value = event.payload['value']
93
+ lane = 0
94
+ if value > 50_000:
95
+ lane = 1
96
+ app.async_invoke("ProcessingTask", event.payload, lane=lane)
97
+
98
+ @app.async_task("ProcessingTask", maximum_concurrency=[10, 2])
99
+ def processing_task(event: ManagedSQSEvent):
100
+ ...
101
+ ```
102
+
103
+ `async-lambda` creates `n` queues and lambda triggers per `async-task` where `n = lane_count`. All of the `n` queues are still consumed by a single lambda function.
104
+
77
105
  ## Unmanaged SQS Task
78
106
 
79
107
  Unmanaged SQS tasks consume from any arbitrary SQS queue (1 message per invocation).
@@ -235,6 +263,20 @@ so that you can reference the extras and the associated managed sqs resource by
235
263
  - `$QUEUEID"` will be replaced with the `LogicalId` of the associated Managed SQS queue.
236
264
  - `$EXTRA<index>` will be replaced with the `LogicalId` of the extra at the specified index.
237
265
 
266
+ ## `method_settings`
267
+
268
+ **This config value can only be set at the app or stage level.**
269
+
270
+ ```
271
+ [
272
+ {...}
273
+ ]
274
+ ```
275
+
276
+ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverless::Api` resource is created.
277
+
278
+ The value is passed into the [`MethodSettings`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-methodsettings) property of the `AWS::Serverless::Api`. The spec for `MethodSetting` can be found [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html).
279
+
238
280
  ## `domain_name`
239
281
 
240
282
  **This config value can only be set at the app or stage level.**
@@ -273,6 +315,22 @@ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverles
273
315
 
274
316
  This config value will set the [`CertificateArn`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-api-domainconfiguration.html) field of the [`Domain` property](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-domain)
275
317
 
318
+ ## `tags`
319
+
320
+ ```
321
+ {
322
+ "TAG_NAME": "TAG_VALUE"
323
+ }
324
+ ```
325
+
326
+ This config value will set the `Tags` field of all resources created by async-lambda. This will not set the field on `managed_queue_extras` resources.
327
+
328
+ The keys `framework` and `framework-version` will always be set and the system values will override any values set by the user.
329
+
330
+ For managed queues the tags `async-lambda-queue-type` will be set to `dlq`, `dlq-task`, or `managed` depending on the queue type.
331
+
332
+ For `async_task` queues (non dlq-task) the `async-lambda-lane` will be set.
333
+
276
334
  # Building an `async-lambda` app
277
335
 
278
336
  **When the app is packaged for lambda, only the main module, and the `vendor` and `src` directories are included.**
@@ -9,4 +9,4 @@ from .models.events.managed_sqs_event import ManagedSQSEvent as ManagedSQSEvent
9
9
  from .models.events.scheduled_event import ScheduledEvent as ScheduledEvent
10
10
  from .models.events.unmanaged_sqs_event import UnmanagedSQSEvent as UnmanagedSQSEvent
11
11
 
12
- __version__ = "0.3.2"
12
+ __version__ = "0.3.5"
@@ -2,6 +2,12 @@ from dataclasses import dataclass
2
2
  from typing import Dict, List, Optional, Set, Union
3
3
 
4
4
 
5
+ def make_default_tags() -> Dict[str, str]:
6
+ from . import __version__
7
+
8
+ return {"framework": "async-lambda", "framework-version": __version__}
9
+
10
+
5
11
  @dataclass
6
12
  class AsyncLambdaBuildConfig:
7
13
  environment_variables: Dict[str, str]
@@ -10,6 +16,8 @@ class AsyncLambdaBuildConfig:
10
16
  subnet_ids: Set[str]
11
17
  security_group_ids: Set[str]
12
18
  managed_queue_extras: List[dict]
19
+ method_settings: List[dict]
20
+ tags: Dict[str, str]
13
21
  domain_name: Optional[str] = None
14
22
  tls_version: Optional[str] = None
15
23
  certificate_arn: Optional[str] = None
@@ -23,6 +31,8 @@ class AsyncLambdaBuildConfig:
23
31
  subnet_ids=set(config.get("subnet_ids", set())),
24
32
  security_group_ids=set(config.get("security_group_ids", set())),
25
33
  managed_queue_extras=list(config.get("managed_queue_extras", list())),
34
+ method_settings=list(config.get("method_settings", list())),
35
+ tags=config.get("tags", dict()),
26
36
  domain_name=config.get("domain_name"),
27
37
  tls_version=config.get("tls_version"),
28
38
  certificate_arn=config.get("certificate_arn"),
@@ -35,6 +45,7 @@ class AsyncLambdaBuildConfig:
35
45
  self.subnet_ids.update(other.subnet_ids)
36
46
  self.security_group_ids.update(other.security_group_ids)
37
47
  self.managed_queue_extras += other.managed_queue_extras
48
+ self.tags.update(other.tags)
38
49
  if other.domain_name is not None:
39
50
  self.domain_name = other.domain_name
40
51
  if other.tls_version is not None:
@@ -52,6 +63,7 @@ def get_build_config_for_stage(
52
63
  stage_config = config.setdefault("stages", {}).setdefault(stage, {})
53
64
  build_config.merge(AsyncLambdaBuildConfig.new(stage_config))
54
65
 
66
+ build_config.tags.update(make_default_tags())
55
67
  return build_config
56
68
 
57
69
 
@@ -72,5 +84,5 @@ def get_build_config_for_task(
72
84
  stage, {}
73
85
  )
74
86
  build_config.merge(AsyncLambdaBuildConfig.new(task_stage_config))
75
-
87
+ build_config.tags.update(make_default_tags())
76
88
  return build_config
@@ -17,21 +17,32 @@ from .models.events.unmanaged_sqs_event import UnmanagedSQSEvent
17
17
  from .models.mock.mock_context import MockLambdaContext
18
18
  from .models.mock.mock_event import MockSQSLambdaEvent
19
19
  from .models.task import AsyncLambdaTask, TaskTriggerType
20
+ from .util import make_cf_tags
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
23
24
 
24
25
  class AsyncLambdaController:
25
26
  is_sub: bool
27
+ lane_count: Optional[int] = None
28
+ propagate_lane_assignment: Optional[bool] = None
26
29
  tasks: Dict[str, AsyncLambdaTask]
27
30
  current_task_id: Optional[str] = None
31
+ current_lane: Optional[int] = None
28
32
  current_invocation_id: Optional[str] = None
29
33
  parent_controller: Optional["AsyncLambdaController"] = None
30
34
  dlq_task_id: Optional[str] = None
31
35
 
32
- def __init__(self, is_sub: bool = False):
36
+ def __init__(
37
+ self,
38
+ is_sub: bool = False,
39
+ lane_count: Optional[int] = None,
40
+ propagate_lane_assignment: Optional[bool] = None,
41
+ ):
33
42
  self.tasks = dict()
34
43
  self.is_sub = is_sub
44
+ self.lane_count = lane_count
45
+ self.propagate_lane_assignment = propagate_lane_assignment
35
46
 
36
47
  def add_task(self, task: AsyncLambdaTask):
37
48
  """
@@ -43,6 +54,21 @@ class AsyncLambdaController:
43
54
  )
44
55
  self.tasks[task.task_id] = task
45
56
 
57
+ def get_lane_count(self) -> int:
58
+ if self.lane_count is not None:
59
+ return self.lane_count
60
+
61
+ if self.parent_controller is not None:
62
+ return self.parent_controller.get_lane_count()
63
+ return 1
64
+
65
+ def should_propagate_lane_assignment(self) -> bool:
66
+ if self.propagate_lane_assignment is not None:
67
+ return self.propagate_lane_assignment
68
+ if self.parent_controller is not None:
69
+ return self.parent_controller.should_propagate_lane_assignment()
70
+ return True
71
+
46
72
  def get_task(self, task_id: str) -> Optional[AsyncLambdaTask]:
47
73
  """
48
74
  Retrieve a task by task_id from this or any parent controllers.
@@ -81,16 +107,26 @@ class AsyncLambdaController:
81
107
  """
82
108
  Generates the SAM Template for this project.
83
109
  """
110
+ build_config = get_build_config_for_stage(config_dict, stage)
84
111
  template = {
85
112
  "AWSTemplateFormatVersion": "2010-09-09",
86
113
  "Transform": "AWS::Serverless-2016-10-31",
87
114
  "Resources": {
88
115
  "AsyncLambdaPayloadBucket": {
89
116
  "Type": "AWS::S3::Bucket",
117
+ "Properties": {"Tags": make_cf_tags(build_config.tags)},
90
118
  },
91
119
  "AsyncLambdaDLQ": {
92
120
  "Type": "AWS::SQS::Queue",
93
- "Properties": {"MessageRetentionPeriod": 1_209_600}, # 14 days
121
+ "Properties": {
122
+ "MessageRetentionPeriod": 1_209_600, # 14 days
123
+ "Tags": make_cf_tags(
124
+ {
125
+ **build_config.tags,
126
+ "async-lambda-queue-type": "dlq",
127
+ }
128
+ ),
129
+ },
94
130
  },
95
131
  },
96
132
  }
@@ -104,14 +140,19 @@ class AsyncLambdaController:
104
140
  ).items():
105
141
  template["Resources"][logical_id] = resource
106
142
 
107
- build_config = get_build_config_for_stage(config_dict, stage)
108
143
  for extra_index, extra in enumerate(build_config.managed_queue_extras):
109
144
  template["Resources"][
110
145
  self._dlq_extra_logical_id(extra_index)
111
146
  ] = self._dlq_extras_replace_references(extra)
112
147
 
113
148
  if has_api_tasks:
114
- properties: dict = {"StageName": "prod"}
149
+ properties: dict = {
150
+ "StageName": "prod",
151
+ "PropagateTags": True,
152
+ "Tags": build_config.tags,
153
+ }
154
+ if len(build_config.method_settings) > 0:
155
+ properties["MethodSettings"] = build_config.method_settings
115
156
  if build_config.domain_name is not None:
116
157
  properties["Domain"] = {
117
158
  "DomainName": build_config.domain_name,
@@ -150,6 +191,17 @@ class AsyncLambdaController:
150
191
  """
151
192
  self.current_task_id = task_id
152
193
 
194
+ def set_current_lane(self, lane: int):
195
+ """
196
+ Set the current lane
197
+ """
198
+ self.current_lane = lane
199
+
200
+ def get_current_lane(self) -> int:
201
+ if self.current_lane is None:
202
+ return 0
203
+ return self.current_lane
204
+
153
205
  def set_current_invocation_id(self, invocation_id: str):
154
206
  """
155
207
  Set the current_invocation_id
@@ -160,6 +212,7 @@ class AsyncLambdaController:
160
212
  """
161
213
  Direct the invocation to the task executor.
162
214
  """
215
+ self.current_lane = None
163
216
  if task_id is None:
164
217
  task_id = env.get_current_task_id()
165
218
  task = self.tasks[task_id]
@@ -168,6 +221,16 @@ class AsyncLambdaController:
168
221
 
169
222
  if task.trigger_type == TaskTriggerType.MANAGED_SQS:
170
223
  _event = ManagedSQSEvent(*args)
224
+ lane_count = task.get_lane_count()
225
+ if lane_count == 1:
226
+ self.set_current_lane(lane=0)
227
+ else:
228
+ for lane_index in range(lane_count):
229
+ if _event.event_source_arn == task.get_managed_queue_arn(
230
+ lane=lane_index
231
+ ):
232
+ self.set_current_lane(lane=lane_index)
233
+ break
171
234
  self.set_current_invocation_id(_event.invocation_id)
172
235
  elif task.trigger_type == TaskTriggerType.UNMANAGED_SQS:
173
236
  _event = UnmanagedSQSEvent(*args)
@@ -192,6 +255,7 @@ class AsyncLambdaController:
192
255
  payload: Any,
193
256
  delay: Optional[int] = None,
194
257
  force_sync: bool = False,
258
+ lane: Optional[int] = None,
195
259
  ):
196
260
  """
197
261
  Invoke an 'async-lambda' task asynchronously utilizing it's SQS queue
@@ -202,16 +266,29 @@ class AsyncLambdaController:
202
266
  payload=payload,
203
267
  delay=delay,
204
268
  force_sync=force_sync,
269
+ lane=lane,
205
270
  )
206
271
  if destination_task_id not in self.tasks:
207
272
  raise Exception(
208
273
  f"No such task exists with the task_id {destination_task_id}"
209
274
  )
275
+
210
276
  destination_task = self.tasks[destination_task_id]
211
277
  if destination_task.trigger_type != TaskTriggerType.MANAGED_SQS:
212
278
  raise Exception(
213
279
  f"Unable to invoke task '{destination_task_id}' because it is a {destination_task.trigger_type} task"
214
280
  )
281
+
282
+ if lane is None and destination_task.should_propagate_lane_assignment():
283
+ lane = self.get_current_lane()
284
+ if lane is None:
285
+ lane = 0
286
+
287
+ if lane < 0 or lane >= destination_task.get_lane_count():
288
+ raise Exception(
289
+ f"Unable to invoke task {destination_task_id} in lane {lane} because it is not a valid lane for the task."
290
+ )
291
+
215
292
  if self.current_invocation_id is None:
216
293
  invocation_id = str(uuid4())
217
294
  else:
@@ -225,14 +302,22 @@ class AsyncLambdaController:
225
302
  )
226
303
 
227
304
  logger.info(
228
- f"Invoking task '{destination_task.task_id}' - invocation_id '{invocation_id}' - delay {delay or 0} - size ({payload_size})"
305
+ f"Invoking task '{destination_task.task_id}' - invocation_id '{invocation_id}' - delay {delay or 0} - size ({payload_size}) - lane {lane}"
229
306
  )
230
307
  if force_sync or env.get_force_sync_mode():
231
308
  if delay:
232
309
  time.sleep(delay)
233
310
  # Sync invocation with mock event/context
234
311
  current_task_id = self.current_task_id
235
- mock_event = MockSQSLambdaEvent(sqs_body)
312
+ queue_arn = None
313
+ if current_task_id is not None:
314
+ current_task = self.get_task(current_task_id)
315
+ if (
316
+ current_task is not None
317
+ and current_task.trigger_type == TaskTriggerType.MANAGED_SQS
318
+ ):
319
+ queue_arn = current_task.get_managed_queue_arn(lane=lane)
320
+ mock_event = MockSQSLambdaEvent(sqs_body, source_queue_arn=queue_arn)
236
321
  mock_context = MockLambdaContext(destination_task.task_id)
237
322
  response = self.handle_invocation(
238
323
  mock_event, mock_context, task_id=destination_task_id
@@ -241,7 +326,7 @@ class AsyncLambdaController:
241
326
  return response
242
327
  else:
243
328
  get_sqs_client().send_message(
244
- QueueUrl=destination_task.get_managed_queue_url(),
329
+ QueueUrl=destination_task.get_managed_queue_url(lane=lane),
245
330
  MessageBody=sqs_body,
246
331
  DelaySeconds=delay,
247
332
  )
@@ -289,6 +374,7 @@ class AsyncLambdaController:
289
374
  payload: Any,
290
375
  delay: Optional[int] = 0,
291
376
  force_sync: bool = False,
377
+ lane: Optional[int] = None,
292
378
  ):
293
379
  """
294
380
  Invoke an Async-Lambda task.
@@ -298,6 +384,7 @@ class AsyncLambdaController:
298
384
  payload=payload,
299
385
  delay=delay,
300
386
  force_sync=force_sync,
387
+ lane=lane,
301
388
  )
302
389
 
303
390
  def async_lambda_handler(self, event, context):
@@ -312,6 +399,8 @@ class AsyncLambdaController:
312
399
  max_receive_count: int = 1,
313
400
  dlq_task_id: Optional[str] = None,
314
401
  is_dlq_task: bool = False,
402
+ lane_count: Optional[int] = None,
403
+ propagate_lane_assignment: Optional[bool] = None,
315
404
  **kwargs,
316
405
  ):
317
406
  """
@@ -337,6 +426,8 @@ class AsyncLambdaController:
337
426
  "max_receive_count": max_receive_count,
338
427
  "dlq_task_id": dlq_task_id,
339
428
  "is_dlq_task": is_dlq_task,
429
+ "lane_count": lane_count,
430
+ "propagate_lane_assignment": propagate_lane_assignment,
340
431
  },
341
432
  **kwargs,
342
433
  )
@@ -3,8 +3,10 @@ from typing import List, Optional, Tuple
3
3
  from urllib.parse import parse_qs, parse_qsl
4
4
 
5
5
 
6
- def MockSQSLambdaEvent(body: str) -> dict:
6
+ def MockSQSLambdaEvent(body: str, source_queue_arn: Optional[str] = None) -> dict:
7
7
  now_timestamp = int(time())
8
+ if not source_queue_arn:
9
+ source_queue_arn = "arn:aws:sqs:us-east-1:123456789012:my-queue"
8
10
  return {
9
11
  "Records": [
10
12
  {
@@ -20,7 +22,7 @@ def MockSQSLambdaEvent(body: str) -> dict:
20
22
  "messageAttributes": {},
21
23
  "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
22
24
  "eventSource": "aws:sqs",
23
- "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:my-queue",
25
+ "eventSourceARN": source_queue_arn,
24
26
  "awsRegion": "us-east-1",
25
27
  }
26
28
  ]
@@ -3,6 +3,8 @@ import re
3
3
  from enum import Enum
4
4
  from typing import TYPE_CHECKING, Any, Callable, Generic, List, Optional, TypeVar, Union
5
5
 
6
+ from async_lambda.util import make_cf_tags
7
+
6
8
  from .. import env
7
9
  from ..build_config import get_build_config_for_task
8
10
  from ..config import config
@@ -36,7 +38,7 @@ class AsyncLambdaTask(Generic[EventType]):
36
38
  timeout: int
37
39
  memory: int
38
40
  ephemeral_storage: int
39
- maximum_concurrency: Optional[int]
41
+ maximum_concurrency: Optional[Union[int, List[int]]]
40
42
 
41
43
  executable: Callable[[EventType], Any]
42
44
 
@@ -50,7 +52,7 @@ class AsyncLambdaTask(Generic[EventType]):
50
52
  timeout: int = 60,
51
53
  memory: int = 128,
52
54
  ephemeral_storage: int = 512,
53
- maximum_concurrency: Optional[int] = None,
55
+ maximum_concurrency: Optional[Union[int, List[int]]] = None,
54
56
  ):
55
57
  AsyncLambdaTask.validate_task_id(task_id)
56
58
  self.controller = controller
@@ -86,61 +88,119 @@ class AsyncLambdaTask(Generic[EventType]):
86
88
  if len(task_id) > 32:
87
89
  raise ValueError("Task ID must be less than 32 characters long.")
88
90
 
89
- def get_managed_queue_name(self):
91
+ def get_lane_count(self) -> int:
92
+ if self.trigger_type != TaskTriggerType.MANAGED_SQS:
93
+ raise Exception(f"The task {self.task_id} is not a managed queue task.")
94
+ if "lane_count" in self.trigger_config and isinstance(
95
+ self.trigger_config["lane_count"], int
96
+ ):
97
+ return self.trigger_config["lane_count"]
98
+ return self.controller.get_lane_count()
99
+
100
+ def should_propagate_lane_assignment(self) -> bool:
101
+ if self.trigger_type != TaskTriggerType.MANAGED_SQS:
102
+ raise Exception(f"The task {self.task_id} is not a managed queue task.")
103
+ if "propagate_lane_assignment" in self.trigger_config and isinstance(
104
+ self.trigger_config["propagate_lane_assignment"], bool
105
+ ):
106
+ return self.trigger_config["propagate_lane_assignment"]
107
+ return self.controller.should_propagate_lane_assignment()
108
+
109
+ def get_managed_queue_name(self, lane: int = 0):
90
110
  """
91
111
  Returns the managed queue's name for this task.
92
112
  """
93
113
  if self.trigger_type != TaskTriggerType.MANAGED_SQS:
94
114
  raise Exception(f"The task {self.task_id} is not a managed queue task.")
95
- return f"{config.name}-{self.task_id}"
115
+ if lane == 0:
116
+ return f"{config.name}-{self.task_id}"
117
+ return f"{config.name}-{self.task_id}-L{lane}"
96
118
 
97
119
  def get_function_name(self):
98
120
  return f"{config.name}-{self.task_id}"
99
121
 
100
- def get_managed_queue_arn(self):
122
+ def get_managed_queue_arn(self, lane: int = 0):
101
123
  if self.trigger_type != TaskTriggerType.MANAGED_SQS:
102
124
  raise Exception(f"The task {self.task_id} is not a managed queue task.")
103
- return f"arn:aws:sqs:{env.get_aws_region()}:{env.get_aws_account_id()}:{self.get_managed_queue_name()}"
125
+ return f"arn:aws:sqs:{env.get_aws_region()}:{env.get_aws_account_id()}:{self.get_managed_queue_name(lane=lane)}"
104
126
 
105
- def get_managed_queue_url(self):
127
+ def get_managed_queue_url(self, lane: int = 0):
106
128
  if self.trigger_type != TaskTriggerType.MANAGED_SQS:
107
129
  raise Exception(f"The task {self.task_id} is not a managed queue task.")
108
- return f"https://sqs.{env.get_aws_region()}.amazonaws.com/{env.get_aws_account_id()}/{self.get_managed_queue_name()}"
130
+ return f"https://sqs.{env.get_aws_region()}.amazonaws.com/{env.get_aws_account_id()}/{self.get_managed_queue_name(lane=lane)}"
109
131
 
110
132
  def get_function_logical_id(self):
111
133
  return f"{self.task_id}ALFunc"
112
134
 
113
- def get_managed_queue_logical_id(self):
135
+ def get_managed_queue_logical_id(self, lane: int = 0):
114
136
  if self.trigger_type != TaskTriggerType.MANAGED_SQS:
115
137
  raise Exception(f"The task {self.task_id} is not a managed queue task.")
116
- return f"{self.task_id}ALQueue"
138
+ if lane == 0:
139
+ return f"{self.task_id}ALQueue"
140
+ return f"{self.task_id}ALQueueL{lane}"
117
141
 
118
- def get_managed_queue_extra_logical_id(self, index: int):
119
- return f"{self.get_function_logical_id()}Extra{index}"
142
+ def get_managed_queue_extra_logical_id(self, index: int, lane: int = 0):
143
+ if self.trigger_type != TaskTriggerType.MANAGED_SQS:
144
+ raise Exception(f"The task {self.task_id} is not a managed queue task.")
145
+ if lane == 0:
146
+ return f"{self.get_function_logical_id()}Extra{index}"
147
+ return f"{self.get_function_logical_id()}Extra{index}L{lane}"
120
148
 
121
- def get_template_events(self):
149
+ def get_managed_queue_event_logical_id(self, lane: int = 0):
150
+ if self.trigger_type != TaskTriggerType.MANAGED_SQS:
151
+ raise Exception(f"The task {self.task_id} is not a managed queue task.")
152
+ if lane == 0:
153
+ return "ManagedSQS"
154
+ return f"ManagedSQSL{lane}"
155
+
156
+ def get_template_events(self) -> dict:
122
157
  sqs_properties = {}
158
+ if (
159
+ isinstance(self.maximum_concurrency, list)
160
+ and self.trigger_type != TaskTriggerType.MANAGED_SQS
161
+ ):
162
+ raise Exception(
163
+ f"Invalid maximum concurrency configuration for task {self.task_id}. Must be an int, not a list of ints. Lanes are only supported for ManagedSQS tasks."
164
+ )
165
+ if (
166
+ isinstance(self.maximum_concurrency, list)
167
+ and self.trigger_type == TaskTriggerType.MANAGED_SQS
168
+ and len(self.maximum_concurrency) != self.get_lane_count()
169
+ ):
170
+ raise Exception(
171
+ f"Invalid maximum concurrency configuration for task {self.task_id}. The list of maximum concurrency must be equal to the # of lanes for the task."
172
+ )
123
173
  if self.maximum_concurrency is not None:
124
174
  sqs_properties["ScalingConfig"] = {
125
175
  "MaximumConcurrency": self.maximum_concurrency
126
176
  }
127
177
  if self.trigger_type == TaskTriggerType.MANAGED_SQS:
128
- return {
129
- "ManagedSQS": {
178
+ events = {}
179
+ for lane_index in range(self.get_lane_count()):
180
+ sqs_properties = {}
181
+ if isinstance(self.maximum_concurrency, list):
182
+ sqs_properties["ScalingConfig"] = {
183
+ "MaximumConcurrency": self.maximum_concurrency[lane_index]
184
+ }
185
+ elif self.maximum_concurrency is not None:
186
+ sqs_properties["ScalingConfig"] = {
187
+ "MaximumConcurrency": self.maximum_concurrency
188
+ }
189
+ events[self.get_managed_queue_event_logical_id(lane=lane_index)] = {
130
190
  "Type": "SQS",
131
191
  "Properties": {
132
192
  "BatchSize": 1,
133
193
  "Enabled": True,
134
194
  "Queue": {
135
195
  "Fn::GetAtt": [
136
- self.get_managed_queue_logical_id(),
196
+ self.get_managed_queue_logical_id(lane=lane_index),
137
197
  "Arn",
138
198
  ]
139
199
  },
140
200
  **sqs_properties,
141
201
  },
142
202
  }
143
- }
203
+ return events
144
204
  elif self.trigger_type == TaskTriggerType.UNMANAGED_SQS:
145
205
  return {
146
206
  "UnmanagedSQS": {
@@ -178,15 +238,16 @@ class AsyncLambdaTask(Generic[EventType]):
178
238
  }
179
239
  raise NotImplementedError()
180
240
 
181
- def get_policy_sqs_resources(self):
241
+ def get_policy_sqs_resources(self) -> List[dict]:
182
242
  if self.trigger_type == TaskTriggerType.MANAGED_SQS:
183
243
  return [
184
244
  {
185
245
  "Fn::GetAtt": [
186
- self.get_managed_queue_logical_id(),
246
+ self.get_managed_queue_logical_id(lane=lane_index),
187
247
  "Arn",
188
248
  ]
189
249
  }
250
+ for lane_index in range(self.get_lane_count())
190
251
  ]
191
252
  elif self.trigger_type == TaskTriggerType.UNMANAGED_SQS:
192
253
  return [self.trigger_config["queue_arn"]]
@@ -223,24 +284,20 @@ class AsyncLambdaTask(Generic[EventType]):
223
284
  },
224
285
  },
225
286
  ]
226
- managed_tasks_logical_ids = [
227
- _task.get_managed_queue_logical_id()
228
- for _task in tasks
229
- if _task.trigger_type == TaskTriggerType.MANAGED_SQS
287
+ managed_tasks_resources = [
288
+ resource
289
+ for task in tasks
290
+ if task.trigger_type == TaskTriggerType.MANAGED_SQS
291
+ for resource in task.get_policy_sqs_resources()
230
292
  ]
231
- if len(managed_tasks_logical_ids) > 0:
293
+ if len(managed_tasks_resources) > 0:
232
294
  policy_statements.append(
233
295
  {
234
296
  "Effect": "Allow",
235
297
  "Action": ["sqs:SendMessage"],
236
298
  "Resource": [
237
- {
238
- "Fn::GetAtt": [
239
- queue_logical_id,
240
- "Arn",
241
- ]
242
- }
243
- for queue_logical_id in managed_tasks_logical_ids
299
+ managed_tasks_resource
300
+ for managed_tasks_resource in managed_tasks_resources
244
301
  ],
245
302
  },
246
303
  )
@@ -260,15 +317,15 @@ class AsyncLambdaTask(Generic[EventType]):
260
317
  )
261
318
  function_properties = {}
262
319
  if len(build_config.layers) > 0:
263
- function_properties["Layers"] = list(build_config.layers)
320
+ function_properties["Layers"] = sorted(build_config.layers)
264
321
  if len(build_config.security_group_ids) > 0 or len(build_config.subnet_ids) > 0:
265
322
  function_properties["VpcConfig"] = {}
266
323
  if len(build_config.security_group_ids) > 0:
267
- function_properties["VpcConfig"]["SecurityGroupIds"] = list(
324
+ function_properties["VpcConfig"]["SecurityGroupIds"] = sorted(
268
325
  build_config.security_group_ids
269
326
  )
270
327
  if len(build_config.subnet_ids) > 0:
271
- function_properties["VpcConfig"]["SubnetIds"] = list(
328
+ function_properties["VpcConfig"]["SubnetIds"] = sorted(
272
329
  build_config.subnet_ids
273
330
  )
274
331
 
@@ -276,6 +333,7 @@ class AsyncLambdaTask(Generic[EventType]):
276
333
  self.get_function_logical_id(): {
277
334
  "Type": "AWS::Serverless::Function",
278
335
  "Properties": {
336
+ "Tags": build_config.tags,
279
337
  "Handler": f"{module}.lambda_handler",
280
338
  "Runtime": config.runtime,
281
339
  "Environment": {
@@ -319,33 +377,50 @@ class AsyncLambdaTask(Generic[EventType]):
319
377
  "Arn",
320
378
  ]
321
379
  }
322
- template[self.get_managed_queue_logical_id()] = {
323
- "Type": "AWS::SQS::Queue",
324
- "Properties": {
325
- "QueueName": self.get_managed_queue_name(),
326
- "RedrivePolicy": {
327
- "deadLetterTargetArn": dead_letter_target_arn,
328
- "maxReceiveCount": self.trigger_config["max_receive_count"],
380
+ for lane_index in range(self.get_lane_count()):
381
+ _extra_tags = {
382
+ "async-lambda-lane": str(lane_index),
383
+ "async-lambda-queue-type": "managed",
384
+ }
385
+ if (
386
+ self.trigger_type == TaskTriggerType.MANAGED_SQS
387
+ and self.trigger_config["is_dlq_task"]
388
+ ):
389
+ _extra_tags["async-lambda-queue-type"] = "dlq-task"
390
+ template[self.get_managed_queue_logical_id(lane=lane_index)] = {
391
+ "Type": "AWS::SQS::Queue",
392
+ "Properties": {
393
+ "Tags": make_cf_tags({**build_config.tags, **_extra_tags}),
394
+ "QueueName": self.get_managed_queue_name(lane=lane_index),
395
+ "RedrivePolicy": {
396
+ "deadLetterTargetArn": dead_letter_target_arn,
397
+ "maxReceiveCount": self.trigger_config["max_receive_count"],
398
+ },
399
+ "VisibilityTimeout": self.timeout,
329
400
  },
330
- "VisibilityTimeout": self.timeout,
331
- },
332
- }
333
- for extra_index, extra in enumerate(build_config.managed_queue_extras):
334
- template[
335
- self.get_managed_queue_extra_logical_id(extra_index)
336
- ] = self._managed_queue_extras_replace_references(extra)
401
+ }
402
+ for extra_index, extra in enumerate(build_config.managed_queue_extras):
403
+ template[
404
+ self.get_managed_queue_extra_logical_id(
405
+ extra_index, lane=lane_index
406
+ )
407
+ ] = self._managed_queue_extras_replace_references(
408
+ extra, lane=lane_index
409
+ )
337
410
 
338
411
  return template
339
412
 
340
- def _managed_queue_extras_replace_references(self, extra: dict) -> dict:
413
+ def _managed_queue_extras_replace_references(self, extra: dict, lane: int) -> dict:
341
414
  stringified_extra = json.dumps(extra)
342
415
  stringified_extra = re.sub(
343
416
  r"\$EXTRA(?P<index>[0-9]+)",
344
- lambda m: self.get_managed_queue_extra_logical_id(int(m.group("index"))),
417
+ lambda m: self.get_managed_queue_extra_logical_id(
418
+ int(m.group("index")), lane=lane
419
+ ),
345
420
  stringified_extra,
346
421
  )
347
422
  stringified_extra = stringified_extra.replace(
348
- "$QUEUEID", self.get_managed_queue_logical_id()
423
+ "$QUEUEID", self.get_managed_queue_logical_id(lane=lane)
349
424
  )
350
425
 
351
426
  return json.loads(stringified_extra)
@@ -0,0 +1,8 @@
1
+ from typing import Dict, List
2
+
3
+
4
+ def make_cf_tags(tags: Dict[str, str]) -> List[Dict[str, str]]:
5
+ _tags = []
6
+ for key, value in tags.items():
7
+ _tags.append({"Key": key, "Value": value})
8
+ return _tags
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-lambda-unstable
3
- Version: 0.3.2
3
+ Version: 0.3.5
4
4
  Summary: A framework for creating AWS Lambda Async Workflows. - Unstable Branch
5
5
  Author-email: "Nuclei, Inc" <engineering@nuclei.ai>
6
6
  Description-Content-Type: text/markdown
@@ -47,7 +47,7 @@ All task decorators share common arguments for configuring the underlying lambda
47
47
  - `memory: int = 128` Sets the memory allocation for the function.
48
48
  - `timeout: int = 60` Sets the timeout for the function (max 900 seconds).
49
49
  - `ephemeral_storage: int = 512` Sets the ephemeral storage allocation for the function.
50
- - `maximum_concurrency: Optional[int] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.)
50
+ - `maximum_concurrency: Optional[int | List[int]] = None` Sets the maximum concurrency value for the SQS trigger for the function. (only applies to `async_task` and `sqs_task` tasks.) When using the `lanes` feature, this can be a list of maximum concurrency for each lane. The length of the list must equal the # of lanes.
51
51
 
52
52
  ## Async Task
53
53
 
@@ -82,6 +82,34 @@ def task_1(event: ManagedSQSEvent):
82
82
  app.async_invoke("Task1", {})
83
83
  ```
84
84
 
85
+ ### Lanes
86
+
87
+ Sometimes you may want multiple "lanes" for events to travel through, especially when you have constrained throughput with `maximum_concurrency`. Utilize the `lanes` feature to open up multiple paths to an `async-task`. This can be useful if you have a large backlog of messages you need to process, but you don't want to interrupt the normal message flow.
88
+
89
+ The # of lanes can be controlled at the controller, sub-controller, and/or task level. With the configuration propagating down the tree, but it can be overridden at any of the levels. The # of lanes can be set with the `lane_count` parameter.
90
+
91
+ By default all usages of `async_invoke` will place the message in the default lane (`0`). To change this specify `lane=` in the `async_invoke` call. By default, any further calls of `async_invoke` down the call stack will continue to put the messages into the same lane if it is available. You can turn of this behavior by setting `propagate_lane_assignment=False` at the controller level.
92
+
93
+ For example, we will use a payload field to determine which lane processing should occur in. We will set the maximum concurrency for the default lane at 10, and for the other lane at `2`.
94
+
95
+ ```python
96
+ app = AsyncLambdaController(lane_count=2)
97
+
98
+ @app.async_task("SwitchBoard")
99
+ def switch_board(event: ManagedSQSEvent):
100
+ value = event.payload['value']
101
+ lane = 0
102
+ if value > 50_000:
103
+ lane = 1
104
+ app.async_invoke("ProcessingTask", event.payload, lane=lane)
105
+
106
+ @app.async_task("ProcessingTask", maximum_concurrency=[10, 2])
107
+ def processing_task(event: ManagedSQSEvent):
108
+ ...
109
+ ```
110
+
111
+ `async-lambda` creates `n` queues and lambda triggers per `async-task` where `n = lane_count`. All of the `n` queues are still consumed by a single lambda function.
112
+
85
113
  ## Unmanaged SQS Task
86
114
 
87
115
  Unmanaged SQS tasks consume from any arbitrary SQS queue (1 message per invocation).
@@ -243,6 +271,20 @@ so that you can reference the extras and the associated managed sqs resource by
243
271
  - `$QUEUEID"` will be replaced with the `LogicalId` of the associated Managed SQS queue.
244
272
  - `$EXTRA<index>` will be replaced with the `LogicalId` of the extra at the specified index.
245
273
 
274
+ ## `method_settings`
275
+
276
+ **This config value can only be set at the app or stage level.**
277
+
278
+ ```
279
+ [
280
+ {...}
281
+ ]
282
+ ```
283
+
284
+ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverless::Api` resource is created.
285
+
286
+ The value is passed into the [`MethodSettings`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-methodsettings) property of the `AWS::Serverless::Api`. The spec for `MethodSetting` can be found [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html).
287
+
246
288
  ## `domain_name`
247
289
 
248
290
  **This config value can only be set at the app or stage level.**
@@ -281,6 +323,22 @@ If your `async-lambda` app contains any `api_task` tasks, then a `AWS::Serverles
281
323
 
282
324
  This config value will set the [`CertificateArn`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-api-domainconfiguration.html) field of the [`Domain` property](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-domain)
283
325
 
326
+ ## `tags`
327
+
328
+ ```
329
+ {
330
+ "TAG_NAME": "TAG_VALUE"
331
+ }
332
+ ```
333
+
334
+ This config value will set the `Tags` field of all resources created by async-lambda. This will not set the field on `managed_queue_extras` resources.
335
+
336
+ The keys `framework` and `framework-version` will always be set and the system values will override any values set by the user.
337
+
338
+ For managed queues the tags `async-lambda-queue-type` will be set to `dlq`, `dlq-task`, or `managed` depending on the queue type.
339
+
340
+ For `async_task` queues (non dlq-task) the `async-lambda-lane` will be set.
341
+
284
342
  # Building an `async-lambda` app
285
343
 
286
344
  **When the app is packaged for lambda, only the main module, and the `vendor` and `src` directories are included.**
@@ -8,6 +8,7 @@ async_lambda/config.py
8
8
  async_lambda/controller.py
9
9
  async_lambda/env.py
10
10
  async_lambda/py.typed
11
+ async_lambda/util.py
11
12
  async_lambda/models/__init__.py
12
13
  async_lambda/models/task.py
13
14
  async_lambda/models/events/__init__.py