async-lambda-unstable 0.6.6__tar.gz → 0.6.7__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.6 → async_lambda_unstable-0.6.7}/PKG-INFO +1 -1
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/__init__.py +1 -1
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/client.py +19 -1
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/controller.py +270 -97
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/env.py +31 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/.gitignore +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/README.md +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/build_config.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/cli.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/config.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/defer.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/middleware.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/__init__.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/api_response.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/case_insensitive_dict.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/__init__.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/api_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/base_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/dynamodb_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/managed_sqs_batch_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/managed_sqs_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/scheduled_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/unmanaged_sqs_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/mock/mock_context.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/mock/mock_event.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/task.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/payload_encoder.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/py.typed +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/util.py +0 -0
- {async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/pyproject.toml +0 -0
|
@@ -21,4 +21,4 @@ from .models.events.managed_sqs_event import ManagedSQSEvent as ManagedSQSEvent
|
|
|
21
21
|
from .models.events.scheduled_event import ScheduledEvent as ScheduledEvent
|
|
22
22
|
from .models.events.unmanaged_sqs_event import UnmanagedSQSEvent as UnmanagedSQSEvent
|
|
23
23
|
|
|
24
|
-
__version__ = "0.6.
|
|
24
|
+
__version__ = "0.6.7"
|
|
@@ -12,6 +12,7 @@ class Clients:
|
|
|
12
12
|
s3_client (Optional[Any]): The AWS S3 client instance.
|
|
13
13
|
sqs_client (Optional[Any]): The AWS SQS client instance.
|
|
14
14
|
sts_client (Optional[Any]): The AWS STS client instance.
|
|
15
|
+
scheduler_client (Optional[Any]): The AWS EventBridge Scheduler client instance.
|
|
15
16
|
|
|
16
17
|
Methods:
|
|
17
18
|
reset():
|
|
@@ -21,14 +22,16 @@ class Clients:
|
|
|
21
22
|
s3_client: Optional[Any] = None
|
|
22
23
|
sqs_client: Optional[Any] = None
|
|
23
24
|
sts_client: Optional[Any] = None
|
|
25
|
+
scheduler_client: Optional[Any] = None
|
|
24
26
|
|
|
25
27
|
def reset(self):
|
|
26
28
|
"""
|
|
27
|
-
Resets the AWS service clients (S3, SQS, STS) by setting them to None.
|
|
29
|
+
Resets the AWS service clients (S3, SQS, STS, Scheduler) by setting them to None.
|
|
28
30
|
"""
|
|
29
31
|
self.s3_client = None
|
|
30
32
|
self.sqs_client = None
|
|
31
33
|
self.sts_client = None
|
|
34
|
+
self.scheduler_client = None
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
clients = Clients()
|
|
@@ -92,3 +95,18 @@ def get_sts_client():
|
|
|
92
95
|
if clients.sts_client is None:
|
|
93
96
|
clients.sts_client = boto3.client("sts", **get_client_kwargs())
|
|
94
97
|
return clients.sts_client
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_scheduler_client():
|
|
101
|
+
"""
|
|
102
|
+
Returns a cached AWS EventBridge Scheduler client instance.
|
|
103
|
+
|
|
104
|
+
If the client does not exist, it creates a new one using boto3 with the provided keyword arguments.
|
|
105
|
+
Subsequent calls will return the cached client.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
boto3.client: An AWS EventBridge Scheduler client instance.
|
|
109
|
+
"""
|
|
110
|
+
if clients.scheduler_client is None:
|
|
111
|
+
clients.scheduler_client = boto3.client("scheduler", **get_client_kwargs())
|
|
112
|
+
return clients.scheduler_client
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import random
|
|
5
5
|
import re
|
|
6
6
|
import time
|
|
7
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from typing import (
|
|
9
9
|
Any,
|
|
10
10
|
Callable,
|
|
@@ -21,7 +21,7 @@ from uuid import uuid4
|
|
|
21
21
|
|
|
22
22
|
from . import env
|
|
23
23
|
from .build_config import get_build_config_for_stage
|
|
24
|
-
from .client import get_s3_client, get_sqs_client
|
|
24
|
+
from .client import get_s3_client, get_scheduler_client, get_sqs_client
|
|
25
25
|
from .config import config
|
|
26
26
|
from .middleware import MET, RT, MiddlewareFunction, MiddlewareRegistration
|
|
27
27
|
from .models.events.api_event import APIEvent
|
|
@@ -39,6 +39,8 @@ from .util import make_cf_tags
|
|
|
39
39
|
|
|
40
40
|
logger = logging.getLogger(__name__)
|
|
41
41
|
|
|
42
|
+
_SQS_MAX_DELAY_SECONDS = 900
|
|
43
|
+
|
|
42
44
|
BaseEventT = TypeVar("BaseEventT", bound=BaseEvent)
|
|
43
45
|
APIEventT = TypeVar("APIEventT", bound=APIEvent)
|
|
44
46
|
ManagedSQSEventT = TypeVar("ManagedSQSEventT", bound=ManagedSQSEvent)
|
|
@@ -67,6 +69,30 @@ class BatchInvokeException(Exception):
|
|
|
67
69
|
super().__init__(msg)
|
|
68
70
|
|
|
69
71
|
|
|
72
|
+
# Exception raised when async_invoke_* is called to a non-internal task with a delay greater than _SQS_MAX_DELAY_SECONDS
|
|
73
|
+
class AsyncInvokeInvalidDelay(Exception):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _BatchEntry:
|
|
78
|
+
enqueue_id: str
|
|
79
|
+
payload: dict
|
|
80
|
+
delay: int
|
|
81
|
+
message_group_id: Optional[str]
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
enqueue_id: str,
|
|
86
|
+
payload: dict,
|
|
87
|
+
delay: int,
|
|
88
|
+
message_group_id: Optional[str],
|
|
89
|
+
):
|
|
90
|
+
self.enqueue_id = enqueue_id
|
|
91
|
+
self.payload = payload
|
|
92
|
+
self.delay = delay
|
|
93
|
+
self.message_group_id = message_group_id
|
|
94
|
+
|
|
95
|
+
|
|
70
96
|
class AsyncLambdaController:
|
|
71
97
|
"""
|
|
72
98
|
AsyncLambdaController manages async tasks, middleware, and invocation logic for the async-lambda framework.
|
|
@@ -454,18 +480,20 @@ class AsyncLambdaController:
|
|
|
454
480
|
},
|
|
455
481
|
}
|
|
456
482
|
_task_list = list(self.tasks.values())
|
|
457
|
-
|
|
483
|
+
internal_tasks_resources = [
|
|
458
484
|
resource
|
|
459
485
|
for task in _task_list
|
|
460
486
|
if task.trigger_type in MANAGED_SQS_TASK_TYPES
|
|
461
487
|
for resource in task.get_policy_sqs_resources()
|
|
462
|
-
]
|
|
488
|
+
]
|
|
489
|
+
external_tasks_resources = [
|
|
463
490
|
resource
|
|
464
491
|
for external_async_task_id in self.external_async_tasks
|
|
465
492
|
for resource in AsyncLambdaTask.get_policy_external_task_resources(
|
|
466
493
|
external_async_task_id
|
|
467
494
|
)
|
|
468
495
|
]
|
|
496
|
+
managed_tasks_resources = internal_tasks_resources + external_tasks_resources
|
|
469
497
|
task_ref_policies = {}
|
|
470
498
|
if len(managed_tasks_resources) > 0:
|
|
471
499
|
task_ref_policies = self._build_send_to_all_async_lambda_queues_policies(
|
|
@@ -474,6 +502,78 @@ class AsyncLambdaController:
|
|
|
474
502
|
for key in task_ref_policies.keys():
|
|
475
503
|
template["Resources"][key] = task_ref_policies[key]
|
|
476
504
|
|
|
505
|
+
if len(internal_tasks_resources) > 0:
|
|
506
|
+
template["Resources"]["AsyncLambdaDelayScheduleGroup"] = {
|
|
507
|
+
"Type": "AWS::Scheduler::ScheduleGroup",
|
|
508
|
+
"Properties": {
|
|
509
|
+
"Name": f"{config.name}-delay",
|
|
510
|
+
"Tags": make_cf_tags(build_config.tags),
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
template["Resources"]["AsyncLambdaDelaySchedulerRole"] = {
|
|
514
|
+
"Type": "AWS::IAM::Role",
|
|
515
|
+
"Properties": {
|
|
516
|
+
"AssumeRolePolicyDocument": {
|
|
517
|
+
"Version": "2012-10-17",
|
|
518
|
+
"Statement": [
|
|
519
|
+
{
|
|
520
|
+
"Effect": "Allow",
|
|
521
|
+
"Principal": {"Service": "scheduler.amazonaws.com"},
|
|
522
|
+
"Action": "sts:AssumeRole",
|
|
523
|
+
}
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
"ManagedPolicyArns": [
|
|
527
|
+
{"Ref": policy_id} for policy_id in task_ref_policies.keys()
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
}
|
|
531
|
+
template["Resources"][
|
|
532
|
+
"AsyncLambdaCreateDelaySchedulesPolicy"
|
|
533
|
+
] = task_ref_policies["AsyncLambdaCreateDelaySchedulesPolicy"] = {
|
|
534
|
+
"Type": "AWS::IAM::ManagedPolicy",
|
|
535
|
+
"Properties": {
|
|
536
|
+
"ManagedPolicyName": {
|
|
537
|
+
"Fn::Sub": "${AWS::StackName}-create-delay-schedules"
|
|
538
|
+
},
|
|
539
|
+
"PolicyDocument": {
|
|
540
|
+
"Version": "2012-10-17",
|
|
541
|
+
"Statement": [
|
|
542
|
+
{
|
|
543
|
+
"Sid": "CreateSchedules",
|
|
544
|
+
"Effect": "Allow",
|
|
545
|
+
"Action": "scheduler:CreateSchedule",
|
|
546
|
+
"Resource": {
|
|
547
|
+
"Fn::Sub": "arn:aws:scheduler:${AWS::Region}:${AWS::AccountId}:schedule/${AsyncLambdaDelayScheduleGroup}/*"
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"Sid": "PassSchedulerExecutionRole",
|
|
552
|
+
"Effect": "Allow",
|
|
553
|
+
"Action": "iam:PassRole",
|
|
554
|
+
"Resource": {
|
|
555
|
+
"Fn::GetAtt": [
|
|
556
|
+
"AsyncLambdaDelaySchedulerRole",
|
|
557
|
+
"Arn",
|
|
558
|
+
]
|
|
559
|
+
},
|
|
560
|
+
"Condition": {
|
|
561
|
+
"StringEquals": {
|
|
562
|
+
"iam:PassedToService": "scheduler.amazonaws.com"
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
}
|
|
570
|
+
template["Globals"]["Function"]["Environment"]["Variables"][
|
|
571
|
+
"ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP"
|
|
572
|
+
] = {"Ref": "AsyncLambdaDelayScheduleGroup"}
|
|
573
|
+
template["Globals"]["Function"]["Environment"]["Variables"][
|
|
574
|
+
"ASYNC_LAMBDA_DELAY_SCHEDULER_ROLE_ARN"
|
|
575
|
+
] = {"Fn::GetAtt": ["AsyncLambdaDelaySchedulerRole", "Arn"]}
|
|
576
|
+
|
|
477
577
|
has_api_tasks = False
|
|
478
578
|
for task in _task_list:
|
|
479
579
|
if task.trigger_type == TaskTriggerType.API_EVENT:
|
|
@@ -808,6 +908,12 @@ class AsyncLambdaController:
|
|
|
808
908
|
raise Exception(
|
|
809
909
|
f"No such task exists with the task_id {destination_task_id}"
|
|
810
910
|
)
|
|
911
|
+
|
|
912
|
+
if is_external_task and delay > _SQS_MAX_DELAY_SECONDS:
|
|
913
|
+
raise AsyncInvokeInvalidDelay(
|
|
914
|
+
f"Unable to invoke task {destination_task_id} with delay {delay} seconds. External tasks only support delays up to {_SQS_MAX_DELAY_SECONDS} seconds."
|
|
915
|
+
)
|
|
916
|
+
|
|
811
917
|
destination_task = None
|
|
812
918
|
if not is_external_task:
|
|
813
919
|
destination_task = self.tasks[destination_task_id]
|
|
@@ -878,16 +984,29 @@ class AsyncLambdaController:
|
|
|
878
984
|
assert destination_task is not None
|
|
879
985
|
url = destination_task.get_managed_queue_url(lane=lane)
|
|
880
986
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
_kwargs["MessageGroupId"] = _message_group_id
|
|
987
|
+
if delay > _SQS_MAX_DELAY_SECONDS:
|
|
988
|
+
assert not is_external_task
|
|
884
989
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
990
|
+
queue_arn = destination_task.get_managed_queue_arn(lane=lane)
|
|
991
|
+
# TODO: Use _message_group_id when AWS tells me how
|
|
992
|
+
self._send_via_scheduler(
|
|
993
|
+
queue_arn=queue_arn,
|
|
994
|
+
message_body=json.dumps(sqs_payload),
|
|
995
|
+
delay=delay,
|
|
996
|
+
message_group_id=_message_group_id,
|
|
997
|
+
)
|
|
998
|
+
else:
|
|
999
|
+
_kwargs = {}
|
|
1000
|
+
if _message_group_id:
|
|
1001
|
+
_kwargs["MessageGroupId"] = _message_group_id
|
|
1002
|
+
|
|
1003
|
+
get_sqs_client().send_message(
|
|
1004
|
+
QueueUrl=url,
|
|
1005
|
+
MessageBody=json.dumps(sqs_payload),
|
|
1006
|
+
DelaySeconds=delay,
|
|
1007
|
+
**_kwargs,
|
|
1008
|
+
)
|
|
1009
|
+
return None
|
|
891
1010
|
|
|
892
1011
|
def send_async_invoke_payload_batch(
|
|
893
1012
|
self,
|
|
@@ -964,6 +1083,35 @@ class AsyncLambdaController:
|
|
|
964
1083
|
)
|
|
965
1084
|
lane = 0
|
|
966
1085
|
|
|
1086
|
+
batch_entries: List[_BatchEntry] = []
|
|
1087
|
+
for i, sqs_payload in enumerate(sqs_payloads):
|
|
1088
|
+
if isinstance(delay, Sequence):
|
|
1089
|
+
_delay = delay[i]
|
|
1090
|
+
else:
|
|
1091
|
+
_delay = delay
|
|
1092
|
+
|
|
1093
|
+
_message_group_id = (
|
|
1094
|
+
message_group_id[i]
|
|
1095
|
+
if isinstance(message_group_id, Sequence)
|
|
1096
|
+
and not isinstance(message_group_id, str)
|
|
1097
|
+
else message_group_id
|
|
1098
|
+
)
|
|
1099
|
+
if (
|
|
1100
|
+
_message_group_id is None
|
|
1101
|
+
and (current_message_group_id := self.get_current_message_group_id())
|
|
1102
|
+
is not None
|
|
1103
|
+
and self.should_propagate_message_group_id()
|
|
1104
|
+
):
|
|
1105
|
+
_message_group_id = current_message_group_id
|
|
1106
|
+
batch_entries.append(
|
|
1107
|
+
_BatchEntry(
|
|
1108
|
+
enqueue_id=f"index_{index + i}",
|
|
1109
|
+
payload=sqs_payload,
|
|
1110
|
+
delay=_delay,
|
|
1111
|
+
message_group_id=_message_group_id,
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
967
1115
|
if force_sync or env.get_force_sync_mode():
|
|
968
1116
|
if is_external_task:
|
|
969
1117
|
raise NotImplementedError(
|
|
@@ -974,34 +1122,16 @@ class AsyncLambdaController:
|
|
|
974
1122
|
current_lane = self.get_current_lane()
|
|
975
1123
|
assert destination_task is not None
|
|
976
1124
|
queue_arn = destination_task.get_managed_queue_arn(lane=lane)
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
else:
|
|
982
|
-
time.sleep(delay)
|
|
983
|
-
|
|
984
|
-
_message_group_id = (
|
|
985
|
-
message_group_id[i]
|
|
986
|
-
if isinstance(message_group_id, Sequence)
|
|
987
|
-
and not isinstance(message_group_id, str)
|
|
988
|
-
else message_group_id
|
|
989
|
-
)
|
|
990
|
-
if (
|
|
991
|
-
_message_group_id is None
|
|
992
|
-
and (
|
|
993
|
-
current_message_group_id := self.get_current_message_group_id()
|
|
994
|
-
)
|
|
995
|
-
is not None
|
|
996
|
-
and self.should_propagate_message_group_id()
|
|
997
|
-
):
|
|
998
|
-
_message_group_id = current_message_group_id
|
|
1125
|
+
|
|
1126
|
+
for batch in batch_entries:
|
|
1127
|
+
if batch.delay:
|
|
1128
|
+
time.sleep(batch.delay)
|
|
999
1129
|
|
|
1000
1130
|
current_message_group_id = self.get_current_message_group_id()
|
|
1001
1131
|
mock_event = MockSQSLambdaEvent(
|
|
1002
|
-
json.dumps(
|
|
1132
|
+
json.dumps(batch.payload),
|
|
1003
1133
|
source_queue_arn=queue_arn,
|
|
1004
|
-
message_group_id=
|
|
1134
|
+
message_group_id=batch.message_group_id,
|
|
1005
1135
|
)
|
|
1006
1136
|
mock_context = MockLambdaContext(destination_task.task_id)
|
|
1007
1137
|
self.handle_invocation(
|
|
@@ -1011,69 +1141,112 @@ class AsyncLambdaController:
|
|
|
1011
1141
|
self.set_current_task_id(current_task_id)
|
|
1012
1142
|
self.set_current_message_group_id(current_message_group_id)
|
|
1013
1143
|
else:
|
|
1014
|
-
|
|
1015
|
-
for
|
|
1016
|
-
if
|
|
1017
|
-
|
|
1144
|
+
sqs_entries: List[dict] = []
|
|
1145
|
+
for batch in batch_entries:
|
|
1146
|
+
if batch.delay > _SQS_MAX_DELAY_SECONDS:
|
|
1147
|
+
if is_external_task:
|
|
1148
|
+
raise AsyncInvokeInvalidDelay(
|
|
1149
|
+
f"send_async_invoke_payload_batch does not support a delay longer than {_SQS_MAX_DELAY_SECONDS} seconds for external tasks"
|
|
1150
|
+
)
|
|
1151
|
+
assert destination_task is not None
|
|
1152
|
+
queue_arn = destination_task.get_managed_queue_arn(lane=lane)
|
|
1153
|
+
self._send_via_scheduler(
|
|
1154
|
+
queue_arn=queue_arn,
|
|
1155
|
+
message_body=json.dumps(batch.payload),
|
|
1156
|
+
delay=batch.delay,
|
|
1157
|
+
)
|
|
1018
1158
|
else:
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1159
|
+
entry: dict = {
|
|
1160
|
+
"MessageBody": json.dumps(batch.payload),
|
|
1161
|
+
"DelaySeconds": batch.delay,
|
|
1162
|
+
"Id": batch.enqueue_id,
|
|
1163
|
+
}
|
|
1164
|
+
if batch.message_group_id:
|
|
1165
|
+
entry["MessageGroupId"] = batch.message_group_id
|
|
1166
|
+
sqs_entries.append(entry)
|
|
1167
|
+
if sqs_entries:
|
|
1168
|
+
if is_external_task:
|
|
1169
|
+
url = f"https://sqs.{env.get_aws_region()}.amazonaws.com/{env.get_aws_account_id()}/{destination_task_id}"
|
|
1170
|
+
else:
|
|
1171
|
+
assert destination_task is not None
|
|
1172
|
+
url = destination_task.get_managed_queue_url(lane=lane)
|
|
1173
|
+
failed_messages: List[dict] = []
|
|
1174
|
+
batch_retry_count = env.get_batch_failure_retry_count() + 1
|
|
1175
|
+
entries = sqs_entries
|
|
1176
|
+
for i in range(batch_retry_count):
|
|
1177
|
+
response = get_sqs_client().send_message_batch(
|
|
1178
|
+
QueueUrl=url,
|
|
1179
|
+
Entries=entries,
|
|
1031
1180
|
)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
response = get_sqs_client().send_message_batch(
|
|
1054
|
-
QueueUrl=url,
|
|
1055
|
-
Entries=entries,
|
|
1181
|
+
failed_messages = response.get("Failed", [])
|
|
1182
|
+
if len(failed_messages) == 0:
|
|
1183
|
+
return
|
|
1184
|
+
logger.warning(failed_messages)
|
|
1185
|
+
logger.warning(f"{len(failed_messages)} messages failed to send. ")
|
|
1186
|
+
failed_message_ids = {message["Id"] for message in failed_messages}
|
|
1187
|
+
entries = [
|
|
1188
|
+
entry for entry in entries if entry["Id"] in failed_message_ids
|
|
1189
|
+
]
|
|
1190
|
+
if i < batch_retry_count:
|
|
1191
|
+
send_delay = 0.5 + random.random()
|
|
1192
|
+
logger.info(
|
|
1193
|
+
f"Waiting {send_delay:.3f} before attempting batch failures again."
|
|
1194
|
+
)
|
|
1195
|
+
time.sleep(send_delay)
|
|
1196
|
+
logger.error(failed_messages)
|
|
1197
|
+
raise BatchInvokeException(
|
|
1198
|
+
f"Failed to send {len(failed_messages)} messages.",
|
|
1199
|
+
failed_payloads=[
|
|
1200
|
+
int(entry["Id"].split("_")[-1]) for entry in entries
|
|
1201
|
+
],
|
|
1056
1202
|
)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1203
|
+
|
|
1204
|
+
@staticmethod
|
|
1205
|
+
def _send_via_scheduler(
|
|
1206
|
+
queue_arn: str,
|
|
1207
|
+
message_body: str,
|
|
1208
|
+
delay: int,
|
|
1209
|
+
message_group_id: Optional[str] = None,
|
|
1210
|
+
):
|
|
1211
|
+
"""
|
|
1212
|
+
Schedules a message for future delivery to an SQS queue via EventBridge Scheduler.
|
|
1213
|
+
|
|
1214
|
+
Used when the requested delay exceeds SQS's maximum of 900 seconds (15 minutes).
|
|
1215
|
+
Creates a one-time schedule in the stack-level schedule group (ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP),
|
|
1216
|
+
which is provisioned by CloudFormation. The schedule auto-deletes after firing.
|
|
1217
|
+
|
|
1218
|
+
Requires the following environment variables:
|
|
1219
|
+
- ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP: name of the CloudFormation-managed schedule group.
|
|
1220
|
+
- ASYNC_LAMBDA_DELAY_SCHEDULER_ROLE_ARN: ARN of the IAM role EventBridge Scheduler uses
|
|
1221
|
+
to deliver to the target SQS queue (must have sqs:SendMessage on all queues in the group).
|
|
1222
|
+
|
|
1223
|
+
Args:
|
|
1224
|
+
queue_arn (str): ARN of the destination SQS queue.
|
|
1225
|
+
message_body (str): JSON-serialized message body to deliver to the queue.
|
|
1226
|
+
delay (int): Delay in seconds before delivering the message.
|
|
1227
|
+
message_group_id (Optional[str]): MessageGroupId for FIFO queues. Defaults to None.
|
|
1228
|
+
"""
|
|
1229
|
+
schedule_time = datetime.now(tz=timezone.utc) + timedelta(seconds=delay)
|
|
1230
|
+
schedule_expression = f"at({schedule_time.strftime('%Y-%m-%dT%H:%M:%S')})"
|
|
1231
|
+
|
|
1232
|
+
target: dict = {
|
|
1233
|
+
"Arn": queue_arn,
|
|
1234
|
+
"RoleArn": env.get_delay_scheduler_role_arn(),
|
|
1235
|
+
"Input": message_body,
|
|
1236
|
+
}
|
|
1237
|
+
# TODO: Set message_group_id based on feedback from AWS
|
|
1238
|
+
# if message_group_id:
|
|
1239
|
+
# target["SqsParameters"] = {"MessageGroupId": message_group_id}
|
|
1240
|
+
|
|
1241
|
+
get_scheduler_client().create_schedule(
|
|
1242
|
+
Name=uuid4().hex,
|
|
1243
|
+
GroupName=env.get_delay_schedule_group_name(),
|
|
1244
|
+
ScheduleExpression=schedule_expression,
|
|
1245
|
+
ScheduleExpressionTimezone="UTC",
|
|
1246
|
+
FlexibleTimeWindow={"Mode": "OFF"},
|
|
1247
|
+
Target=target,
|
|
1248
|
+
ActionAfterCompletion="DELETE",
|
|
1249
|
+
)
|
|
1077
1250
|
|
|
1078
1251
|
def new_payload(
|
|
1079
1252
|
self,
|
|
@@ -206,3 +206,34 @@ def get_batch_failure_retry_count() -> int:
|
|
|
206
206
|
int: The number of times to retry batch failures. Defaults to 20 if the environment variable is not set.
|
|
207
207
|
"""
|
|
208
208
|
return int(os.environ.get("ASYNC_LAMBDA_BATCH_FAILURE_RETRY_COUNT", 20))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_delay_scheduler_role_arn() -> str:
|
|
212
|
+
"""
|
|
213
|
+
Retrieves the IAM role ARN used by EventBridge Scheduler to send messages to SQS.
|
|
214
|
+
|
|
215
|
+
The role must have sqs:SendMessage permission on the target queues.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
str: The value of the 'ASYNC_LAMBDA_DELAY_SCHEDULER_ROLE_ARN' environment variable.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
KeyError: If 'ASYNC_LAMBDA_DELAY_SCHEDULER_ROLE_ARN' is not set in the environment.
|
|
222
|
+
"""
|
|
223
|
+
return os.environ["ASYNC_LAMBDA_DELAY_SCHEDULER_ROLE_ARN"]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_delay_schedule_group_name() -> str:
|
|
227
|
+
"""
|
|
228
|
+
Retrieves the EventBridge Scheduler delay schedule group name for this stack.
|
|
229
|
+
|
|
230
|
+
One group is provisioned per CloudFormation stack (set via AWS::Scheduler::ScheduleGroup
|
|
231
|
+
in the SAM template) and injected as ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP when there internally managed queues.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
str: The value of the 'ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP' environment variable.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
KeyError: If 'ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP' is not set in the environment.
|
|
238
|
+
"""
|
|
239
|
+
return os.environ["ASYNC_LAMBDA_DELAY_SCHEDULE_GROUP"]
|
|
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.6 → async_lambda_unstable-0.6.7}/async_lambda/models/api_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/__init__.py
RENAMED
|
File without changes
|
{async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/api_event.py
RENAMED
|
File without changes
|
{async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/events/base_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/mock/mock_context.py
RENAMED
|
File without changes
|
{async_lambda_unstable-0.6.6 → async_lambda_unstable-0.6.7}/async_lambda/models/mock/mock_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|