async-lambda-unstable 0.6.13__tar.gz → 0.6.15__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.6.13 → async_lambda_unstable-0.6.15}/PKG-INFO +48 -1
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/README.md +47 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/__init__.py +1 -1
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/build_config.py +56 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/cli.py +18 -1
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/controller.py +124 -3
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/task.py +34 -2
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/.gitignore +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/client.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/config.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/defer.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/env.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/middleware.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/__init__.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/api_response.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/case_insensitive_dict.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/__init__.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/api_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/base_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/dynamodb_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/managed_sqs_batch_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/managed_sqs_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/scheduled_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/unmanaged_sqs_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/mock/mock_context.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/mock/mock_event.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/payload_encoder.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/py.typed +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/util.py +0 -0
- {async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: async-lambda-unstable
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.15
|
|
4
4
|
Summary: A framework for creating AWS Lambda Async Workflows. - Unstable Branch
|
|
5
5
|
Author-email: "Nuclei, Inc" <engineering@nuclei.ai>
|
|
6
6
|
Requires-Dist: click>=8.0.0
|
|
@@ -249,6 +249,32 @@ and merging is done thru concatenation.
|
|
|
249
249
|
|
|
250
250
|
[The value is passed to the `Layers` property on `SAM::Serverless::Function`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-layers)
|
|
251
251
|
|
|
252
|
+
### image_uri
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
"ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY:TAG"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Package the functions as a Lambda container image instead of a zip bundle, which raises the
|
|
259
|
+
package size limit from 250 MB (unzipped) to 10 GB.
|
|
260
|
+
|
|
261
|
+
When set, every function gets `PackageType: Image`, `ImageUri`, and `ImageConfig.Command` set to
|
|
262
|
+
`<module>.lambda_handler` in place of `CodeUri`, `Handler`, and `Runtime`, and `async-lambda build`
|
|
263
|
+
writes only the template (no `deployment.zip`). The image is built and pushed separately; it must
|
|
264
|
+
be based on an AWS Lambda Python base image (or implement the Lambda runtime interface) and contain
|
|
265
|
+
the app module, `src/`, and its dependencies.
|
|
266
|
+
|
|
267
|
+
`async-lambda build app --image-uri <uri>` sets this for a single build, overriding the config (including
|
|
268
|
+
stage-level values), so a CI pipeline can pin the image it just pushed.
|
|
269
|
+
|
|
270
|
+
Image functions are named `<name>-<task_id>-image`. Lambda cannot change an existing function's package
|
|
271
|
+
type, so adding or removing `image_uri` replaces each function, and CloudFormation cannot replace a
|
|
272
|
+
function under the same custom name. The suffix lets the new function be created before the old one is
|
|
273
|
+
deleted, so switching needs no manual deletion or downtime. Queue and schedule names are unchanged.
|
|
274
|
+
|
|
275
|
+
Lambda layers and SnapStart are not supported on container images, so `layers` and `snap_start`
|
|
276
|
+
raise an error at build time when `image_uri` is set.
|
|
277
|
+
|
|
252
278
|
### subnet_ids
|
|
253
279
|
|
|
254
280
|
```
|
|
@@ -437,6 +463,27 @@ Two caveats:
|
|
|
437
463
|
- A task with task-specific `policies` keeps its own generated role, since the shared role is built from app-wide and stage-wide config only.
|
|
438
464
|
- Every app-wide policy must be either a managed policy ARN or a `{"Statement": [...]}` document. SAM policy templates can't be expressed in a plain IAM role and will fail the build.
|
|
439
465
|
|
|
466
|
+
### Consolidated Queue-Age Alarms
|
|
467
|
+
|
|
468
|
+
`managed_queue_extras` attaches a copy of each extra resource to *every* managed queue, so using it for a CloudWatch alarm costs one stack resource per queue lane.
|
|
469
|
+
|
|
470
|
+
Setting `queue_age_alarm` in the build config emits a small number of grouped alarms instead, each watching `ApproximateAgeOfOldestMessage` across up to ten queues:
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"queue_age_alarm": {
|
|
475
|
+
"threshold": 5000,
|
|
476
|
+
"period": 86400,
|
|
477
|
+
"alarm_actions": ["arn:aws:sns:us-east-1:123456789012:alarms"],
|
|
478
|
+
"ok_actions": ["arn:aws:sns:us-east-1:123456789012:alarms"]
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Only `threshold` is required and `period` defaults to one day. Coverage is every managed queue lane plus the app DLQ. An app with 104 queues gets 11 alarms rather than 104.
|
|
484
|
+
|
|
485
|
+
CloudWatch rejects an alarm carrying more than 10 metrics.
|
|
486
|
+
|
|
440
487
|
## Known Limitations
|
|
441
488
|
|
|
442
489
|
- Not all Lambda configuration options are supported (see code for extension points)
|
|
@@ -237,6 +237,32 @@ and merging is done thru concatenation.
|
|
|
237
237
|
|
|
238
238
|
[The value is passed to the `Layers` property on `SAM::Serverless::Function`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-layers)
|
|
239
239
|
|
|
240
|
+
### image_uri
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
"ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY:TAG"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Package the functions as a Lambda container image instead of a zip bundle, which raises the
|
|
247
|
+
package size limit from 250 MB (unzipped) to 10 GB.
|
|
248
|
+
|
|
249
|
+
When set, every function gets `PackageType: Image`, `ImageUri`, and `ImageConfig.Command` set to
|
|
250
|
+
`<module>.lambda_handler` in place of `CodeUri`, `Handler`, and `Runtime`, and `async-lambda build`
|
|
251
|
+
writes only the template (no `deployment.zip`). The image is built and pushed separately; it must
|
|
252
|
+
be based on an AWS Lambda Python base image (or implement the Lambda runtime interface) and contain
|
|
253
|
+
the app module, `src/`, and its dependencies.
|
|
254
|
+
|
|
255
|
+
`async-lambda build app --image-uri <uri>` sets this for a single build, overriding the config (including
|
|
256
|
+
stage-level values), so a CI pipeline can pin the image it just pushed.
|
|
257
|
+
|
|
258
|
+
Image functions are named `<name>-<task_id>-image`. Lambda cannot change an existing function's package
|
|
259
|
+
type, so adding or removing `image_uri` replaces each function, and CloudFormation cannot replace a
|
|
260
|
+
function under the same custom name. The suffix lets the new function be created before the old one is
|
|
261
|
+
deleted, so switching needs no manual deletion or downtime. Queue and schedule names are unchanged.
|
|
262
|
+
|
|
263
|
+
Lambda layers and SnapStart are not supported on container images, so `layers` and `snap_start`
|
|
264
|
+
raise an error at build time when `image_uri` is set.
|
|
265
|
+
|
|
240
266
|
### subnet_ids
|
|
241
267
|
|
|
242
268
|
```
|
|
@@ -425,6 +451,27 @@ Two caveats:
|
|
|
425
451
|
- A task with task-specific `policies` keeps its own generated role, since the shared role is built from app-wide and stage-wide config only.
|
|
426
452
|
- Every app-wide policy must be either a managed policy ARN or a `{"Statement": [...]}` document. SAM policy templates can't be expressed in a plain IAM role and will fail the build.
|
|
427
453
|
|
|
454
|
+
### Consolidated Queue-Age Alarms
|
|
455
|
+
|
|
456
|
+
`managed_queue_extras` attaches a copy of each extra resource to *every* managed queue, so using it for a CloudWatch alarm costs one stack resource per queue lane.
|
|
457
|
+
|
|
458
|
+
Setting `queue_age_alarm` in the build config emits a small number of grouped alarms instead, each watching `ApproximateAgeOfOldestMessage` across up to ten queues:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"queue_age_alarm": {
|
|
463
|
+
"threshold": 5000,
|
|
464
|
+
"period": 86400,
|
|
465
|
+
"alarm_actions": ["arn:aws:sns:us-east-1:123456789012:alarms"],
|
|
466
|
+
"ok_actions": ["arn:aws:sns:us-east-1:123456789012:alarms"]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Only `threshold` is required and `period` defaults to one day. Coverage is every managed queue lane plus the app DLQ. An app with 104 queues gets 11 alarms rather than 104.
|
|
472
|
+
|
|
473
|
+
CloudWatch rejects an alarm carrying more than 10 metrics.
|
|
474
|
+
|
|
428
475
|
## Known Limitations
|
|
429
476
|
|
|
430
477
|
- Not all Lambda configuration options are supported (see code for extension points)
|
|
@@ -22,4 +22,4 @@ from .models.events.managed_sqs_event import ManagedSQSEvent as ManagedSQSEvent
|
|
|
22
22
|
from .models.events.scheduled_event import ScheduledEvent as ScheduledEvent
|
|
23
23
|
from .models.events.unmanaged_sqs_event import UnmanagedSQSEvent as UnmanagedSQSEvent
|
|
24
24
|
|
|
25
|
-
__version__ = "0.6.
|
|
25
|
+
__version__ = "0.6.15"
|
|
@@ -14,6 +14,46 @@ def make_default_tags() -> Dict[str, str]:
|
|
|
14
14
|
return {"framework": "async-lambda", "framework-version": __version__}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
@dataclass
|
|
18
|
+
class QueueAgeAlarmConfig:
|
|
19
|
+
"""
|
|
20
|
+
Settings for the consolidated queue-age alarms.
|
|
21
|
+
|
|
22
|
+
One alarm covers a group of queues rather than one alarm per queue.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
threshold (int): Oldest-message age, in seconds, above which the alarm fires.
|
|
26
|
+
period (int): Seconds of data each datapoint covers.
|
|
27
|
+
alarm_actions (List[str]): ARNs notified when an alarm starts firing.
|
|
28
|
+
ok_actions (List[str]): ARNs notified when an alarm recovers.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
threshold: int
|
|
32
|
+
period: int
|
|
33
|
+
alarm_actions: List[str]
|
|
34
|
+
ok_actions: List[str]
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def new(cls, config: dict) -> "QueueAgeAlarmConfig":
|
|
38
|
+
"""
|
|
39
|
+
Creates a new instance from a configuration dictionary.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config (dict): provided the values of QueueAgeAlarmConfig
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
QueueAgeAlarmConfig: A new instance configured with the provided options.
|
|
46
|
+
"""
|
|
47
|
+
if "threshold" not in config:
|
|
48
|
+
raise ValueError("queue_age_alarm requires a `threshold`.")
|
|
49
|
+
return cls(
|
|
50
|
+
threshold=config["threshold"],
|
|
51
|
+
period=config.get("period", 86400),
|
|
52
|
+
alarm_actions=list(config.get("alarm_actions", list())),
|
|
53
|
+
ok_actions=list(config.get("ok_actions", list())),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
17
57
|
@dataclass
|
|
18
58
|
class AsyncLambdaBuildConfig:
|
|
19
59
|
"""
|
|
@@ -26,6 +66,7 @@ class AsyncLambdaBuildConfig:
|
|
|
26
66
|
subnet_ids (Set[str]): Set of subnet IDs for VPC configuration.
|
|
27
67
|
security_group_ids (Set[str]): Set of security group IDs for VPC configuration.
|
|
28
68
|
managed_queue_extras (List[dict]): Additional configuration for managed queues.
|
|
69
|
+
queue_age_alarm (Optional[QueueAgeAlarmConfig]): Settings for the consolidated queue-age alarms. No alarms are emitted when this is unset.
|
|
29
70
|
method_settings (List[dict]): API Gateway method settings.
|
|
30
71
|
tags (Dict[str, str]): Tags to assign to the Lambda function.
|
|
31
72
|
logging_config (Dict[str, str]): Logging configuration options.
|
|
@@ -34,6 +75,7 @@ class AsyncLambdaBuildConfig:
|
|
|
34
75
|
certificate_arn (Optional[str]): ARN of the SSL certificate for the domain.
|
|
35
76
|
hosted_zone_id (Optional[str]): Hosted zone name for the Lambda/API.
|
|
36
77
|
auto_create_acm_certificate (bool): Whether or not to auto create a certificate in ACM for the domain name. Must be used in conjunction with hosted_zone_id and domain_name to be used.
|
|
78
|
+
image_uri (Optional[str]): URI of a Lambda container image. When set, functions are packaged as container images instead of a zip bundle.
|
|
37
79
|
|
|
38
80
|
Methods:
|
|
39
81
|
new(config: dict) -> AsyncLambdaBuildConfig:
|
|
@@ -57,6 +99,8 @@ class AsyncLambdaBuildConfig:
|
|
|
57
99
|
certificate_arn: Optional[str] = None
|
|
58
100
|
hosted_zone_id: Optional[str] = None
|
|
59
101
|
auto_create_acm_certificate: Optional[bool] = None
|
|
102
|
+
queue_age_alarm: Optional[QueueAgeAlarmConfig] = None
|
|
103
|
+
image_uri: Optional[str] = None
|
|
60
104
|
|
|
61
105
|
@classmethod
|
|
62
106
|
def new(cls, config: dict) -> "AsyncLambdaBuildConfig":
|
|
@@ -71,6 +115,7 @@ class AsyncLambdaBuildConfig:
|
|
|
71
115
|
- subnet_ids (set or list): Set or list of subnet IDs for VPC configuration.
|
|
72
116
|
- security_group_ids (set or list): Set or list of security group IDs for VPC configuration.
|
|
73
117
|
- managed_queue_extras (list): Additional managed queue configuration.
|
|
118
|
+
- queue_age_alarm (dict): Settings for the consolidated queue-age alarms.
|
|
74
119
|
- method_settings (list): List of method settings for API Gateway.
|
|
75
120
|
- tags (dict): Tags to assign to the Lambda function.
|
|
76
121
|
- logging_config (dict): Logging configuration options.
|
|
@@ -78,6 +123,7 @@ class AsyncLambdaBuildConfig:
|
|
|
78
123
|
- tls_version (str, optional): TLS version to use.
|
|
79
124
|
- certificate_arn (str, optional): ARN of the SSL certificate.
|
|
80
125
|
- hosted_zone_id (str, optional): Hosted zone ID for the Lambda/API.
|
|
126
|
+
- image_uri (str, optional): URI of the Lambda container image.
|
|
81
127
|
|
|
82
128
|
Returns:
|
|
83
129
|
AsyncLambdaBuildConfig: A new instance configured with the provided options.
|
|
@@ -89,6 +135,11 @@ class AsyncLambdaBuildConfig:
|
|
|
89
135
|
subnet_ids=set(config.get("subnet_ids", set())),
|
|
90
136
|
security_group_ids=set(config.get("security_group_ids", set())),
|
|
91
137
|
managed_queue_extras=list(config.get("managed_queue_extras", list())),
|
|
138
|
+
queue_age_alarm=(
|
|
139
|
+
QueueAgeAlarmConfig.new(config["queue_age_alarm"])
|
|
140
|
+
if config.get("queue_age_alarm") is not None
|
|
141
|
+
else None
|
|
142
|
+
),
|
|
92
143
|
method_settings=list(config.get("method_settings", list())),
|
|
93
144
|
tags=config.get("tags", dict()),
|
|
94
145
|
logging_config=config.get("logging_config", dict()),
|
|
@@ -97,6 +148,7 @@ class AsyncLambdaBuildConfig:
|
|
|
97
148
|
certificate_arn=config.get("certificate_arn"),
|
|
98
149
|
hosted_zone_id=config.get("hosted_zone_id"),
|
|
99
150
|
auto_create_acm_certificate=config.get("auto_create_acm_certificate"),
|
|
151
|
+
image_uri=config.get("image_uri"),
|
|
100
152
|
)
|
|
101
153
|
|
|
102
154
|
def merge(self, other: "AsyncLambdaBuildConfig"):
|
|
@@ -118,6 +170,10 @@ class AsyncLambdaBuildConfig:
|
|
|
118
170
|
self.hosted_zone_id = other.hosted_zone_id
|
|
119
171
|
if other.auto_create_acm_certificate is not None:
|
|
120
172
|
self.auto_create_acm_certificate = other.auto_create_acm_certificate
|
|
173
|
+
if other.queue_age_alarm is not None:
|
|
174
|
+
self.queue_age_alarm = other.queue_age_alarm
|
|
175
|
+
if other.image_uri is not None:
|
|
176
|
+
self.image_uri = other.image_uri
|
|
121
177
|
|
|
122
178
|
@property
|
|
123
179
|
def function_properties(self):
|
|
@@ -108,8 +108,16 @@ def import_module_get_controller(module_name: str, config, stage: Optional[str])
|
|
|
108
108
|
@click.option(
|
|
109
109
|
"-e", "--extras", help="The path to a json file to merge the output template with."
|
|
110
110
|
)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--image-uri",
|
|
113
|
+
help="Package the functions as this Lambda container image instead of a zip bundle. Overrides `image_uri` in the config.",
|
|
114
|
+
)
|
|
111
115
|
def build(
|
|
112
|
-
module: str,
|
|
116
|
+
module: str,
|
|
117
|
+
output: str,
|
|
118
|
+
stage: Optional[str] = None,
|
|
119
|
+
extras: Optional[str] = None,
|
|
120
|
+
image_uri: Optional[str] = None,
|
|
113
121
|
):
|
|
114
122
|
"""
|
|
115
123
|
Builds/generates the SAM template for the given module.
|
|
@@ -119,6 +127,10 @@ def build(
|
|
|
119
127
|
config_file = dir.joinpath(".async_lambda/config.json")
|
|
120
128
|
if config_file.exists():
|
|
121
129
|
config = json.loads(config_file.read_bytes())
|
|
130
|
+
if image_uri is not None:
|
|
131
|
+
config["image_uri"] = image_uri
|
|
132
|
+
for stage_config in config.get("stages", {}).values():
|
|
133
|
+
stage_config.pop("image_uri", None)
|
|
122
134
|
controller = import_module_get_controller(
|
|
123
135
|
module_name=module, config=config, stage=stage
|
|
124
136
|
)
|
|
@@ -157,6 +169,11 @@ def build(
|
|
|
157
169
|
|
|
158
170
|
if os.path.exists(".async_lambda/build"):
|
|
159
171
|
shutil.rmtree(".async_lambda/build")
|
|
172
|
+
|
|
173
|
+
if get_build_config_for_stage(config, stage).image_uri is not None:
|
|
174
|
+
click.echo("Container image configured, skipping zip bundle.")
|
|
175
|
+
return
|
|
176
|
+
|
|
160
177
|
os.makedirs(".async_lambda/build/packages", exist_ok=True)
|
|
161
178
|
|
|
162
179
|
if dir.joinpath("requirements.txt").exists():
|
|
@@ -20,7 +20,11 @@ from typing import (
|
|
|
20
20
|
from uuid import uuid4
|
|
21
21
|
|
|
22
22
|
from . import env
|
|
23
|
-
from .build_config import
|
|
23
|
+
from .build_config import (
|
|
24
|
+
AsyncLambdaBuildConfig,
|
|
25
|
+
QueueAgeAlarmConfig,
|
|
26
|
+
get_build_config_for_stage,
|
|
27
|
+
)
|
|
24
28
|
from .client import get_s3_client, get_scheduler_client, get_sqs_client
|
|
25
29
|
from .config import config
|
|
26
30
|
from .middleware import MET, RT, MiddlewareFunction, MiddlewareRegistration
|
|
@@ -46,6 +50,9 @@ logger = logging.getLogger(__name__)
|
|
|
46
50
|
|
|
47
51
|
_SQS_MAX_DELAY_SECONDS = 900
|
|
48
52
|
|
|
53
|
+
# CloudWatch rejects an alarm carrying more than 10 metrics.
|
|
54
|
+
_MAX_METRICS_PER_ALARM = 10
|
|
55
|
+
|
|
49
56
|
BaseEventT = TypeVar("BaseEventT", bound=BaseEvent)
|
|
50
57
|
APIEventT = TypeVar("APIEventT", bound=APIEvent)
|
|
51
58
|
ManagedSQSEventT = TypeVar("ManagedSQSEventT", bound=ManagedSQSEvent)
|
|
@@ -231,6 +238,92 @@ class AsyncLambdaController:
|
|
|
231
238
|
+ f"{config.name}-*"
|
|
232
239
|
}
|
|
233
240
|
|
|
241
|
+
def _alarmable_queue_logical_ids(self) -> List[str]:
|
|
242
|
+
"""
|
|
243
|
+
Logical IDs of every queue the queue-age alarms should watch.
|
|
244
|
+
|
|
245
|
+
Covers each managed queue lane plus the app DLQ. The delay DLQ is left out
|
|
246
|
+
because it is named deterministically and so can be alarmed on directly.
|
|
247
|
+
"""
|
|
248
|
+
logical_ids = []
|
|
249
|
+
for task in self.tasks.values():
|
|
250
|
+
if task.trigger_type not in MANAGED_SQS_TASK_TYPES:
|
|
251
|
+
continue
|
|
252
|
+
for lane_index in range(task.get_lane_count()):
|
|
253
|
+
logical_ids.append(task.get_managed_queue_logical_id(lane=lane_index))
|
|
254
|
+
logical_ids.append("AsyncLambdaDLQ")
|
|
255
|
+
return logical_ids
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _build_queue_age_alarms(
|
|
259
|
+
queue_logical_ids: List[str],
|
|
260
|
+
alarm_config: QueueAgeAlarmConfig,
|
|
261
|
+
) -> Dict[str, dict]:
|
|
262
|
+
"""
|
|
263
|
+
Builds alarms watching the oldest message age across every managed queue.
|
|
264
|
+
|
|
265
|
+
Queues are grouped rather than alarmed individually.
|
|
266
|
+
|
|
267
|
+
Each alarm watches `MAX(METRICS())` over its own group. `METRICS()` resolves to just that alarm's
|
|
268
|
+
metrics.
|
|
269
|
+
|
|
270
|
+
Grouping is required because CloudWatch caps an alarm at 10 metrics.
|
|
271
|
+
"""
|
|
272
|
+
alarms = {}
|
|
273
|
+
for alarm_index, group_start in enumerate(
|
|
274
|
+
range(0, len(queue_logical_ids), _MAX_METRICS_PER_ALARM)
|
|
275
|
+
):
|
|
276
|
+
group = queue_logical_ids[
|
|
277
|
+
group_start : group_start + _MAX_METRICS_PER_ALARM
|
|
278
|
+
]
|
|
279
|
+
metrics: List[dict] = [
|
|
280
|
+
{
|
|
281
|
+
"Id": f"q{metric_index}",
|
|
282
|
+
"ReturnData": False,
|
|
283
|
+
"MetricStat": {
|
|
284
|
+
"Metric": {
|
|
285
|
+
"Namespace": "AWS/SQS",
|
|
286
|
+
"MetricName": "ApproximateAgeOfOldestMessage",
|
|
287
|
+
"Dimensions": [
|
|
288
|
+
{
|
|
289
|
+
"Name": "QueueName",
|
|
290
|
+
"Value": {
|
|
291
|
+
"Fn::GetAtt": [queue_logical_id, "QueueName"]
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
"Period": alarm_config.period,
|
|
297
|
+
"Stat": "Maximum",
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
for metric_index, queue_logical_id in enumerate(group)
|
|
301
|
+
]
|
|
302
|
+
metrics.append(
|
|
303
|
+
{
|
|
304
|
+
"Id": "oldest_message_age",
|
|
305
|
+
"Expression": "MAX(METRICS())",
|
|
306
|
+
"ReturnData": True,
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
alarms[f"AsyncLambdaQueueAgeAlarm{alarm_index}"] = {
|
|
310
|
+
"Type": "AWS::CloudWatch::Alarm",
|
|
311
|
+
"Properties": {
|
|
312
|
+
"ActionsEnabled": True,
|
|
313
|
+
"AlarmActions": alarm_config.alarm_actions,
|
|
314
|
+
"OKActions": alarm_config.ok_actions,
|
|
315
|
+
"AlarmDescription": (
|
|
316
|
+
f"Oldest message age across {len(group)} {config.name} queues"
|
|
317
|
+
),
|
|
318
|
+
"ComparisonOperator": "GreaterThanThreshold",
|
|
319
|
+
"EvaluationPeriods": 1,
|
|
320
|
+
"Metrics": metrics,
|
|
321
|
+
"Threshold": alarm_config.threshold,
|
|
322
|
+
"TreatMissingData": "notBreaching",
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
return alarms
|
|
326
|
+
|
|
234
327
|
@classmethod
|
|
235
328
|
def _build_shared_function_role(
|
|
236
329
|
cls,
|
|
@@ -527,6 +620,26 @@ class AsyncLambdaController:
|
|
|
527
620
|
Generates the SAM Template for this project.
|
|
528
621
|
"""
|
|
529
622
|
build_config = get_build_config_for_stage(config_dict, stage)
|
|
623
|
+
if build_config.image_uri is not None and len(build_config.layers) > 0:
|
|
624
|
+
raise Exception(
|
|
625
|
+
"Lambda layers cannot be used with container images (image_uri is set)."
|
|
626
|
+
)
|
|
627
|
+
if build_config.image_uri is not None and any(
|
|
628
|
+
task.snap_start for task in self.tasks.values()
|
|
629
|
+
):
|
|
630
|
+
raise Exception(
|
|
631
|
+
"SnapStart cannot be used with container images (image_uri is set)."
|
|
632
|
+
)
|
|
633
|
+
# SAM Globals does not accept PackageType or ImageConfig, so image functions
|
|
634
|
+
# carry their package properties individually (see AsyncLambdaTask.get_sam_template).
|
|
635
|
+
package_properties = (
|
|
636
|
+
{}
|
|
637
|
+
if build_config.image_uri is not None
|
|
638
|
+
else {
|
|
639
|
+
"Handler": f"{module}.lambda_handler",
|
|
640
|
+
"Runtime": config.runtime,
|
|
641
|
+
}
|
|
642
|
+
)
|
|
530
643
|
s3_bucket_properties = {}
|
|
531
644
|
if config.s3_payload_retention:
|
|
532
645
|
s3_bucket_properties["LifecycleConfiguration"] = {
|
|
@@ -545,8 +658,7 @@ class AsyncLambdaController:
|
|
|
545
658
|
"Globals": {
|
|
546
659
|
"Function": {
|
|
547
660
|
"Tags": build_config.tags,
|
|
548
|
-
|
|
549
|
-
"Runtime": config.runtime,
|
|
661
|
+
**package_properties,
|
|
550
662
|
"Environment": {
|
|
551
663
|
"Variables": {
|
|
552
664
|
"ASYNC_LAMBDA_PAYLOAD_S3_BUCKET": {
|
|
@@ -736,6 +848,7 @@ class AsyncLambdaController:
|
|
|
736
848
|
task_ref_policy_ids=list(task_ref_policies.keys()),
|
|
737
849
|
config_dict=config_dict,
|
|
738
850
|
stage=stage,
|
|
851
|
+
module=module,
|
|
739
852
|
).items():
|
|
740
853
|
template["Resources"][logical_id] = resource
|
|
741
854
|
|
|
@@ -744,6 +857,14 @@ class AsyncLambdaController:
|
|
|
744
857
|
self._dlq_extras_replace_references(extra)
|
|
745
858
|
)
|
|
746
859
|
|
|
860
|
+
if build_config.queue_age_alarm is not None:
|
|
861
|
+
template["Resources"].update(
|
|
862
|
+
self._build_queue_age_alarms(
|
|
863
|
+
queue_logical_ids=self._alarmable_queue_logical_ids(),
|
|
864
|
+
alarm_config=build_config.queue_age_alarm,
|
|
865
|
+
)
|
|
866
|
+
)
|
|
867
|
+
|
|
747
868
|
if has_api_tasks:
|
|
748
869
|
properties: dict = {
|
|
749
870
|
"StageName": "prod",
|
|
@@ -14,7 +14,11 @@ from typing import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
from .. import env
|
|
17
|
-
from ..build_config import
|
|
17
|
+
from ..build_config import (
|
|
18
|
+
get_build_config_for_stage,
|
|
19
|
+
get_build_config_for_task,
|
|
20
|
+
get_override_build_config_for_task,
|
|
21
|
+
)
|
|
18
22
|
from ..config import config
|
|
19
23
|
from ..util import make_cf_tags
|
|
20
24
|
|
|
@@ -28,6 +32,8 @@ from .events.managed_sqs_event import ManagedSQSEvent
|
|
|
28
32
|
from .events.scheduled_event import ScheduledEvent
|
|
29
33
|
from .events.unmanaged_sqs_event import UnmanagedSQSEvent
|
|
30
34
|
|
|
35
|
+
IMAGE_FUNCTION_NAME_SUFFIX = "-image"
|
|
36
|
+
|
|
31
37
|
|
|
32
38
|
class TaskTriggerType(Enum):
|
|
33
39
|
"""
|
|
@@ -548,6 +554,7 @@ class AsyncLambdaTask(Generic[EventType, RT]):
|
|
|
548
554
|
task_ref_policy_ids: List[str],
|
|
549
555
|
config_dict: dict,
|
|
550
556
|
stage: Optional[str] = None,
|
|
557
|
+
module: Optional[str] = None,
|
|
551
558
|
) -> dict:
|
|
552
559
|
"""
|
|
553
560
|
Generate an AWS SAM (Serverless Application Model) template for the current async lambda task.
|
|
@@ -568,6 +575,19 @@ class AsyncLambdaTask(Generic[EventType, RT]):
|
|
|
568
575
|
override_config = get_override_build_config_for_task(
|
|
569
576
|
config=config_dict, task_id=self.task_id, stage=stage
|
|
570
577
|
)
|
|
578
|
+
if override_config.image_uri is not None:
|
|
579
|
+
raise Exception(
|
|
580
|
+
f"Task {self.task_id}: image_uri can only be set at the app or stage level, "
|
|
581
|
+
"because every task is packaged the same way."
|
|
582
|
+
)
|
|
583
|
+
image_uri = get_build_config_for_stage(config_dict, stage).image_uri
|
|
584
|
+
if image_uri is not None:
|
|
585
|
+
if len(build_config.layers) > 0:
|
|
586
|
+
raise Exception(
|
|
587
|
+
f"Task {self.task_id}: Lambda layers cannot be used with container images (image_uri is set)."
|
|
588
|
+
)
|
|
589
|
+
if module is None:
|
|
590
|
+
raise Exception("A module name is required to build image functions.")
|
|
571
591
|
events = self.get_template_events()
|
|
572
592
|
policy_sqs_resources = self.get_policy_sqs_resources()
|
|
573
593
|
|
|
@@ -622,8 +642,20 @@ class AsyncLambdaTask(Generic[EventType, RT]):
|
|
|
622
642
|
"FunctionName": self.get_function_name(),
|
|
623
643
|
"Timeout": self.timeout,
|
|
624
644
|
"Events": events,
|
|
625
|
-
"CodeUri": ".async_lambda/build/deployment.zip",
|
|
626
645
|
}
|
|
646
|
+
if image_uri is not None:
|
|
647
|
+
# Lambda cannot change a function's package type in place, so switching between zip
|
|
648
|
+
# and image replaces the function, and a replacement cannot reuse the old name.
|
|
649
|
+
function_properties["FunctionName"] = (
|
|
650
|
+
f"{self.get_function_name()}{IMAGE_FUNCTION_NAME_SUFFIX}"
|
|
651
|
+
)
|
|
652
|
+
function_properties["PackageType"] = "Image"
|
|
653
|
+
function_properties["ImageUri"] = image_uri
|
|
654
|
+
function_properties["ImageConfig"] = {
|
|
655
|
+
"Command": [f"{module}.lambda_handler"]
|
|
656
|
+
}
|
|
657
|
+
else:
|
|
658
|
+
function_properties["CodeUri"] = ".async_lambda/build/deployment.zip"
|
|
627
659
|
|
|
628
660
|
# cannot use shared role if the task has its own policies
|
|
629
661
|
if config.shared_function_role and len(override_config.policies) == 0:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/__init__.py
RENAMED
|
File without changes
|
{async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/api_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/events/__init__.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
|
|
File without changes
|
{async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/models/mock/mock_event.py
RENAMED
|
File without changes
|
{async_lambda_unstable-0.6.13 → async_lambda_unstable-0.6.15}/async_lambda/payload_encoder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|