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.
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/PKG-INFO +60 -2
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/README.md +59 -1
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/__init__.py +1 -1
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/build_config.py +13 -1
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/controller.py +98 -7
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_event.py +4 -2
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/task.py +126 -51
- async-lambda-unstable-0.3.5/async_lambda/util.py +8 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/PKG-INFO +60 -2
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/SOURCES.txt +1 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/cli.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/client.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/config.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/env.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/__init__.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/__init__.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/api_event.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/base_event.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/managed_sqs_event.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/scheduled_event.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/unmanaged_sqs_event.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_context.py +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/py.typed +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/dependency_links.txt +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/entry_points.txt +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/requires.txt +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/top_level.txt +0 -0
- {async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/pyproject.toml +0 -0
- {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.
|
|
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.
|
|
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__(
|
|
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": {
|
|
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 = {
|
|
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
|
-
|
|
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
|
)
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_event.py
RENAMED
|
@@ -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":
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
for
|
|
229
|
-
if
|
|
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(
|
|
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
|
-
|
|
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"] =
|
|
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"] =
|
|
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"] =
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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(
|
|
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)
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda_unstable.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: async-lambda-unstable
|
|
3
|
-
Version: 0.3.
|
|
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.**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/__init__.py
RENAMED
|
File without changes
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/api_event.py
RENAMED
|
File without changes
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/events/base_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{async-lambda-unstable-0.3.2 → async-lambda-unstable-0.3.5}/async_lambda/models/mock/mock_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|