localstack-core 4.6.1.dev30__py3-none-any.whl → 4.6.1.dev31__py3-none-any.whl
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.
- localstack/services/lambda_/event_source_mapping/esm_worker_factory.py +5 -5
- localstack/services/lambda_/invocation/assignment.py +1 -23
- localstack/services/lambda_/invocation/counting_service.py +0 -21
- localstack/services/lambda_/invocation/docker_runtime_executor.py +5 -4
- localstack/services/lambda_/invocation/execution_environment.py +1 -18
- localstack/services/lambda_/invocation/executor_endpoint.py +4 -4
- localstack/services/lambda_/invocation/lambda_models.py +1 -0
- localstack/services/lambda_/invocation/lambda_service.py +3 -0
- localstack/services/lambda_/invocation/version_manager.py +25 -0
- localstack/services/lambda_/lambda_debug_mode/ldm.py +375 -0
- localstack/services/lambda_/lambda_debug_mode/ldm_config_file.py +178 -0
- localstack/services/lambda_/lambda_debug_mode/ldm_types.py +11 -0
- localstack/services/lambda_/lambda_debug_mode/ldm_utils.py +43 -0
- localstack/services/lambda_/provider.py +14 -14
- localstack/version.py +2 -2
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/METADATA +1 -1
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/RECORD +26 -25
- localstack_core-4.6.1.dev31.dist-info/plux.json +1 -0
- localstack/utils/lambda_debug_mode/lambda_debug_mode.py +0 -41
- localstack/utils/lambda_debug_mode/lambda_debug_mode_config.py +0 -230
- localstack/utils/lambda_debug_mode/lambda_debug_mode_session.py +0 -175
- localstack_core-4.6.1.dev30.dist-info/plux.json +0 -1
- /localstack/{utils → services/lambda_}/lambda_debug_mode/__init__.py +0 -0
- {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack +0 -0
- {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/WHEEL +0 -0
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/top_level.txt +0 -0
|
@@ -37,12 +37,12 @@ from localstack.services.lambda_.event_source_mapping.pollers.sqs_poller import
|
|
|
37
37
|
SqsPoller,
|
|
38
38
|
)
|
|
39
39
|
from localstack.services.lambda_.event_source_mapping.senders.lambda_sender import LambdaSender
|
|
40
|
-
from localstack.
|
|
41
|
-
from localstack.utils.aws.client_types import ServicePrincipal
|
|
42
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import (
|
|
40
|
+
from localstack.services.lambda_.lambda_debug_mode.ldm import (
|
|
43
41
|
DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS,
|
|
44
|
-
|
|
42
|
+
LDM,
|
|
45
43
|
)
|
|
44
|
+
from localstack.utils.aws.arns import parse_arn
|
|
45
|
+
from localstack.utils.aws.client_types import ServicePrincipal
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class PollerHolder:
|
|
@@ -65,7 +65,7 @@ class EsmWorkerFactory:
|
|
|
65
65
|
# Sender (always Lambda)
|
|
66
66
|
function_arn = self.esm_config["FunctionArn"]
|
|
67
67
|
|
|
68
|
-
if
|
|
68
|
+
if LDM.is_enabled():
|
|
69
69
|
timeout_seconds = DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
70
70
|
else:
|
|
71
71
|
# 900s is the maximum amount of time a Lambda can run for.
|
|
@@ -15,10 +15,6 @@ from localstack.services.lambda_.invocation.lambda_models import (
|
|
|
15
15
|
InitializationType,
|
|
16
16
|
OtherServiceEndpoint,
|
|
17
17
|
)
|
|
18
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import (
|
|
19
|
-
is_lambda_debug_enabled_for,
|
|
20
|
-
is_lambda_debug_timeout_enabled_for,
|
|
21
|
-
)
|
|
22
18
|
|
|
23
19
|
LOG = logging.getLogger(__name__)
|
|
24
20
|
|
|
@@ -79,10 +75,7 @@ class AssignmentService(OtherServiceEndpoint):
|
|
|
79
75
|
|
|
80
76
|
try:
|
|
81
77
|
yield execution_environment
|
|
82
|
-
|
|
83
|
-
self.stop_environment(execution_environment)
|
|
84
|
-
else:
|
|
85
|
-
execution_environment.release()
|
|
78
|
+
execution_environment.release()
|
|
86
79
|
except InvalidStatusException as invalid_e:
|
|
87
80
|
LOG.error("InvalidStatusException: %s", invalid_e)
|
|
88
81
|
except Exception as e:
|
|
@@ -143,21 +136,6 @@ class AssignmentService(OtherServiceEndpoint):
|
|
|
143
136
|
function_version: FunctionVersion,
|
|
144
137
|
target_provisioned_environments: int,
|
|
145
138
|
) -> list[Future[None]]:
|
|
146
|
-
# Enforce a single environment per lambda version if this is a target
|
|
147
|
-
# of an active Lambda Debug Mode.
|
|
148
|
-
qualified_lambda_version_arn = function_version.qualified_arn
|
|
149
|
-
if (
|
|
150
|
-
is_lambda_debug_enabled_for(qualified_lambda_version_arn)
|
|
151
|
-
and target_provisioned_environments > 0
|
|
152
|
-
):
|
|
153
|
-
LOG.warning(
|
|
154
|
-
"Environments for '%s' enforced to '1' by Lambda Debug Mode, "
|
|
155
|
-
"configurations will continue to report the set value '%s'",
|
|
156
|
-
qualified_lambda_version_arn,
|
|
157
|
-
target_provisioned_environments,
|
|
158
|
-
)
|
|
159
|
-
target_provisioned_environments = 1
|
|
160
|
-
|
|
161
139
|
current_provisioned_environments = [
|
|
162
140
|
e
|
|
163
141
|
for e in self.environments[version_manager_id].values()
|
|
@@ -11,9 +11,6 @@ from localstack.services.lambda_.invocation.lambda_models import (
|
|
|
11
11
|
InitializationType,
|
|
12
12
|
)
|
|
13
13
|
from localstack.services.lambda_.invocation.models import lambda_stores
|
|
14
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import (
|
|
15
|
-
is_lambda_debug_enabled_for,
|
|
16
|
-
)
|
|
17
14
|
|
|
18
15
|
LOG = logging.getLogger(__name__)
|
|
19
16
|
|
|
@@ -130,24 +127,6 @@ class CountingService:
|
|
|
130
127
|
unqualified_function_arn = function_version.id.unqualified_arn()
|
|
131
128
|
qualified_arn = function_version.id.qualified_arn()
|
|
132
129
|
|
|
133
|
-
# Enforce one lease per ARN if the global flag is set
|
|
134
|
-
if is_lambda_debug_enabled_for(qualified_arn):
|
|
135
|
-
with provisioned_tracker.lock, on_demand_tracker.lock:
|
|
136
|
-
on_demand_executions: int = on_demand_tracker.concurrent_executions[
|
|
137
|
-
unqualified_function_arn
|
|
138
|
-
]
|
|
139
|
-
provisioned_executions = provisioned_tracker.concurrent_executions[qualified_arn]
|
|
140
|
-
if on_demand_executions or provisioned_executions:
|
|
141
|
-
LOG.warning(
|
|
142
|
-
"Concurrent lambda invocations disabled for '%s' by Lambda Debug Mode",
|
|
143
|
-
qualified_arn,
|
|
144
|
-
)
|
|
145
|
-
raise TooManyRequestsException(
|
|
146
|
-
"Rate Exceeded.",
|
|
147
|
-
Reason="SingleLeaseEnforcement",
|
|
148
|
-
Type="User",
|
|
149
|
-
)
|
|
150
|
-
|
|
151
130
|
lease_type = None
|
|
152
131
|
# HACK: skip reserved and provisioned concurrency if function not available (e.g., in Lambda@Edge)
|
|
153
132
|
if function is not None:
|
|
@@ -23,6 +23,7 @@ from localstack.services.lambda_.invocation.runtime_executor import (
|
|
|
23
23
|
LambdaRuntimeException,
|
|
24
24
|
RuntimeExecutor,
|
|
25
25
|
)
|
|
26
|
+
from localstack.services.lambda_.lambda_debug_mode.ldm import LDM_ENV_VAR_DEBUG_PORT
|
|
26
27
|
from localstack.services.lambda_.lambda_utils import HINT_LOG
|
|
27
28
|
from localstack.services.lambda_.networking import (
|
|
28
29
|
get_all_container_networks_for_lambda,
|
|
@@ -43,7 +44,6 @@ from localstack.utils.container_utils.container_client import (
|
|
|
43
44
|
)
|
|
44
45
|
from localstack.utils.docker_utils import DOCKER_CLIENT as CONTAINER_CLIENT
|
|
45
46
|
from localstack.utils.files import chmod_r, rm_rf
|
|
46
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import lambda_debug_port_for
|
|
47
47
|
from localstack.utils.net import get_free_tcp_port
|
|
48
48
|
from localstack.utils.strings import short_uid, truncate
|
|
49
49
|
|
|
@@ -321,9 +321,6 @@ class DockerRuntimeExecutor(RuntimeExecutor):
|
|
|
321
321
|
platform=docker_platform(self.function_version.config.architectures[0]),
|
|
322
322
|
additional_flags=config.LAMBDA_DOCKER_FLAGS,
|
|
323
323
|
)
|
|
324
|
-
debug_port = lambda_debug_port_for(self.function_version.qualified_arn)
|
|
325
|
-
if debug_port is not None:
|
|
326
|
-
container_config.ports.add(debug_port, debug_port)
|
|
327
324
|
|
|
328
325
|
if self.function_version.config.package_type == PackageType.Zip:
|
|
329
326
|
if self.function_version.config.code.is_hot_reloading():
|
|
@@ -379,6 +376,10 @@ class DockerRuntimeExecutor(RuntimeExecutor):
|
|
|
379
376
|
container_config.ports = PortMappings()
|
|
380
377
|
container_config.ports.add(config.LAMBDA_INIT_DELVE_PORT, config.LAMBDA_INIT_DELVE_PORT)
|
|
381
378
|
|
|
379
|
+
if ldm_debug_port := env_vars.get(LDM_ENV_VAR_DEBUG_PORT):
|
|
380
|
+
ldm_debug_port = int(ldm_debug_port)
|
|
381
|
+
container_config.ports.add(ldm_debug_port, ldm_debug_port)
|
|
382
|
+
|
|
382
383
|
if (
|
|
383
384
|
self.function_version.config.layers
|
|
384
385
|
and not config.LAMBDA_PREBUILD_IMAGES
|
|
@@ -20,10 +20,6 @@ from localstack.services.lambda_.invocation.runtime_executor import (
|
|
|
20
20
|
RuntimeExecutor,
|
|
21
21
|
get_runtime_executor,
|
|
22
22
|
)
|
|
23
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import (
|
|
24
|
-
DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS,
|
|
25
|
-
is_lambda_debug_timeout_enabled_for,
|
|
26
|
-
)
|
|
27
23
|
from localstack.utils.strings import to_str
|
|
28
24
|
from localstack.utils.xray.trace_header import TraceHeader
|
|
29
25
|
|
|
@@ -139,7 +135,7 @@ class ExecutionEnvironment:
|
|
|
139
135
|
# AWS_LAMBDA_DOTNET_PREJIT
|
|
140
136
|
"TZ": ":UTC",
|
|
141
137
|
# 2) Public AWS RIE interface: https://github.com/aws/aws-lambda-runtime-interface-emulator
|
|
142
|
-
"AWS_LAMBDA_FUNCTION_TIMEOUT": self.
|
|
138
|
+
"AWS_LAMBDA_FUNCTION_TIMEOUT": self.function_version.config.timeout,
|
|
143
139
|
# 3) Public LocalStack endpoint
|
|
144
140
|
"LOCALSTACK_HOSTNAME": self.runtime_executor.get_endpoint_from_executor(),
|
|
145
141
|
"EDGE_PORT": str(config.GATEWAY_LISTEN[0].port),
|
|
@@ -388,18 +384,5 @@ class ExecutionEnvironment:
|
|
|
388
384
|
DurationSeconds=43200,
|
|
389
385
|
)["Credentials"]
|
|
390
386
|
|
|
391
|
-
def _get_execution_timeout_seconds(self) -> int:
|
|
392
|
-
# Returns the timeout value in seconds to be enforced during the execution of the
|
|
393
|
-
# lambda function. This is the configured value or the DEBUG MODE default if this
|
|
394
|
-
# is enabled.
|
|
395
|
-
if is_lambda_debug_timeout_enabled_for(self.function_version.qualified_arn):
|
|
396
|
-
return DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
397
|
-
return self.function_version.config.timeout
|
|
398
|
-
|
|
399
387
|
def _get_startup_timeout_seconds(self) -> int:
|
|
400
|
-
# Returns the timeout value in seconds to be enforced during lambda container startups.
|
|
401
|
-
# This is the value defined through LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT or the LAMBDA
|
|
402
|
-
# DEBUG MODE default if this is enabled.
|
|
403
|
-
if is_lambda_debug_timeout_enabled_for(self.function_version.qualified_arn):
|
|
404
|
-
return DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
405
388
|
return STARTUP_TIMEOUT_SEC
|
|
@@ -11,11 +11,11 @@ from werkzeug import Request
|
|
|
11
11
|
from localstack.http import Response, route
|
|
12
12
|
from localstack.services.edge import ROUTER
|
|
13
13
|
from localstack.services.lambda_.invocation.lambda_models import InvocationResult
|
|
14
|
-
from localstack.
|
|
15
|
-
from localstack.utils.lambda_debug_mode.lambda_debug_mode import (
|
|
14
|
+
from localstack.services.lambda_.lambda_debug_mode.ldm import (
|
|
16
15
|
DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS,
|
|
17
|
-
|
|
16
|
+
LDM,
|
|
18
17
|
)
|
|
18
|
+
from localstack.utils.backoff import ExponentialBackoff
|
|
19
19
|
from localstack.utils.objects import singleton_factory
|
|
20
20
|
from localstack.utils.strings import to_str
|
|
21
21
|
|
|
@@ -209,7 +209,7 @@ class ExecutorEndpoint(Endpoint):
|
|
|
209
209
|
# Note that if timeouts are enforced for the lambda function invoked at this endpoint
|
|
210
210
|
# (this is needs to be configured in the Lambda Debug Mode Config file), the lambda
|
|
211
211
|
# function will continue to enforce the expected timeouts.
|
|
212
|
-
if
|
|
212
|
+
if LDM.is_enabled():
|
|
213
213
|
# The value is set to a default high value to ensure eventual termination.
|
|
214
214
|
timeout_seconds = DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
215
215
|
else:
|
|
@@ -230,6 +230,7 @@ class LambdaService:
|
|
|
230
230
|
request_id: str,
|
|
231
231
|
payload: bytes | None,
|
|
232
232
|
trace_context: dict | None = None,
|
|
233
|
+
user_agent: Optional[str] = None,
|
|
233
234
|
) -> InvocationResult | None:
|
|
234
235
|
"""
|
|
235
236
|
Invokes a specific version of a lambda
|
|
@@ -352,6 +353,7 @@ class LambdaService:
|
|
|
352
353
|
invoke_time=datetime.now(),
|
|
353
354
|
request_id=request_id,
|
|
354
355
|
trace_context=trace_context,
|
|
356
|
+
user_agent=user_agent,
|
|
355
357
|
)
|
|
356
358
|
)
|
|
357
359
|
|
|
@@ -364,6 +366,7 @@ class LambdaService:
|
|
|
364
366
|
invoke_time=datetime.now(),
|
|
365
367
|
request_id=request_id,
|
|
366
368
|
trace_context=trace_context,
|
|
369
|
+
user_agent=user_agent,
|
|
367
370
|
)
|
|
368
371
|
)
|
|
369
372
|
status = (
|
|
@@ -29,6 +29,7 @@ from localstack.services.lambda_.invocation.metrics import (
|
|
|
29
29
|
record_cw_metric_invocation,
|
|
30
30
|
)
|
|
31
31
|
from localstack.services.lambda_.invocation.runtime_executor import get_runtime_executor
|
|
32
|
+
from localstack.services.lambda_.lambda_debug_mode.ldm import LDM
|
|
32
33
|
from localstack.utils.strings import long_uid, truncate
|
|
33
34
|
from localstack.utils.threads import FuncThread, start_thread
|
|
34
35
|
|
|
@@ -191,6 +192,30 @@ class LambdaVersionManager:
|
|
|
191
192
|
LOG.warning(message)
|
|
192
193
|
raise ServiceException(message)
|
|
193
194
|
|
|
195
|
+
# If the environment has debugging enabled, route the invocation there;
|
|
196
|
+
# debug environments bypass Lambda service quotas.
|
|
197
|
+
if debug_execution_environment := LDM.get_execution_environment(
|
|
198
|
+
qualified_lambda_arn=self.function_version.qualified_arn,
|
|
199
|
+
user_agent=invocation.user_agent,
|
|
200
|
+
):
|
|
201
|
+
try:
|
|
202
|
+
invocation_result = debug_execution_environment.invoke(invocation)
|
|
203
|
+
invocation_result.executed_version = self.function_version.id.qualifier
|
|
204
|
+
self.store_logs(
|
|
205
|
+
invocation_result=invocation_result, execution_env=debug_execution_environment
|
|
206
|
+
)
|
|
207
|
+
except StatusErrorException as e:
|
|
208
|
+
invocation_result = InvocationResult(
|
|
209
|
+
request_id="",
|
|
210
|
+
payload=e.payload,
|
|
211
|
+
is_error=True,
|
|
212
|
+
logs="",
|
|
213
|
+
executed_version=self.function_version.id.qualifier,
|
|
214
|
+
)
|
|
215
|
+
finally:
|
|
216
|
+
debug_execution_environment.release()
|
|
217
|
+
return invocation_result
|
|
218
|
+
|
|
194
219
|
with self.counting_service.get_invocation_lease(
|
|
195
220
|
self.function, self.function_version
|
|
196
221
|
) as provisioning_type:
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from threading import Event, RLock, Thread
|
|
8
|
+
from typing import Callable, Dict, Final, Optional
|
|
9
|
+
|
|
10
|
+
from localstack.aws.api.lambda_ import (
|
|
11
|
+
Arn,
|
|
12
|
+
FunctionVersion,
|
|
13
|
+
ResourceNotFoundException,
|
|
14
|
+
TooManyRequestsException,
|
|
15
|
+
)
|
|
16
|
+
from localstack.config import LAMBDA_DEBUG_MODE, LAMBDA_DEBUG_MODE_CONFIG_PATH
|
|
17
|
+
from localstack.services.lambda_.invocation.execution_environment import (
|
|
18
|
+
ExecutionEnvironment,
|
|
19
|
+
InvalidStatusException,
|
|
20
|
+
RuntimeStatus,
|
|
21
|
+
)
|
|
22
|
+
from localstack.services.lambda_.lambda_debug_mode.ldm_config_file import parse_ldm_config
|
|
23
|
+
from localstack.services.lambda_.provider_utils import get_function_version
|
|
24
|
+
from localstack.utils.aws.arns import parse_arn
|
|
25
|
+
from localstack.utils.objects import singleton_factory
|
|
26
|
+
from localstack.utils.strings import long_uid
|
|
27
|
+
|
|
28
|
+
LOG = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Specifies the default timeout value in seconds to be used by time restricted workflows
|
|
31
|
+
# when Debug Mode is enabled. The value is set to one hour to ensure eventual termination
|
|
32
|
+
# of long-running processes.
|
|
33
|
+
DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS: int = 3_600
|
|
34
|
+
LDM_ENV_VAR_DEBUG_PORT: str = "LDM_DEBUG_PORT"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LambdaFunctionDebugConfig:
|
|
38
|
+
qualified_lambda_arn: Final[Arn]
|
|
39
|
+
port: Final[int]
|
|
40
|
+
enforce_timeouts: Final[bool]
|
|
41
|
+
user_agent: Final[Optional[str]]
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
qualified_lambda_arn: Arn,
|
|
46
|
+
port: int,
|
|
47
|
+
enforce_timeouts: bool = False,
|
|
48
|
+
user_agent: Optional[str] = None,
|
|
49
|
+
):
|
|
50
|
+
self.qualified_lambda_arn = qualified_lambda_arn
|
|
51
|
+
self.port = port
|
|
52
|
+
self.enforce_timeouts = enforce_timeouts
|
|
53
|
+
self.user_agent = user_agent
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DebugEnabledExecutionEnvironment(ExecutionEnvironment):
|
|
57
|
+
_lambda_function_debug_config: Final[LambdaFunctionDebugConfig]
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
function_version: FunctionVersion,
|
|
62
|
+
lambda_function_debug_config: LambdaFunctionDebugConfig,
|
|
63
|
+
on_timeout: Callable[[str, str], None],
|
|
64
|
+
):
|
|
65
|
+
super().__init__(
|
|
66
|
+
function_version=function_version,
|
|
67
|
+
version_manager_id=f"debug-enable-{long_uid()}",
|
|
68
|
+
initialization_type="provisioned-concurrency",
|
|
69
|
+
on_timeout=on_timeout,
|
|
70
|
+
)
|
|
71
|
+
self._lambda_function_debug_config = lambda_function_debug_config
|
|
72
|
+
|
|
73
|
+
def get_environment_variables(self) -> Dict[str, str]:
|
|
74
|
+
environment_variables = super().get_environment_variables()
|
|
75
|
+
environment_variables[LDM_ENV_VAR_DEBUG_PORT] = str(self._lambda_function_debug_config.port)
|
|
76
|
+
if not self._lambda_function_debug_config.enforce_timeouts:
|
|
77
|
+
environment_variables["AWS_LAMBDA_FUNCTION_TIMEOUT"] = str(
|
|
78
|
+
DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
79
|
+
)
|
|
80
|
+
return environment_variables
|
|
81
|
+
|
|
82
|
+
def _get_startup_timeout_seconds(self) -> int:
|
|
83
|
+
return DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LambdaDebugTarget:
|
|
87
|
+
_mutex: Final[RLock]
|
|
88
|
+
_lambda_qualified_arn: Final[str]
|
|
89
|
+
lambda_function_debug_config: Final[LambdaFunctionDebugConfig]
|
|
90
|
+
_debug_execution_environment: Optional[DebugEnabledExecutionEnvironment]
|
|
91
|
+
|
|
92
|
+
def __init__(self, lambda_function_debug_config: LambdaFunctionDebugConfig):
|
|
93
|
+
self._mutex = RLock()
|
|
94
|
+
self._lambda_qualified_arn = lambda_function_debug_config.qualified_lambda_arn
|
|
95
|
+
self.lambda_function_debug_config = lambda_function_debug_config
|
|
96
|
+
self._debug_execution_environment = None
|
|
97
|
+
|
|
98
|
+
def start_debug_enabled_execution_environment(self):
|
|
99
|
+
# Attempt to create the debug environment now if the function exists.
|
|
100
|
+
with self._mutex:
|
|
101
|
+
if self._debug_execution_environment is not None:
|
|
102
|
+
return
|
|
103
|
+
self.stop_debug_enabled_execution_environment()
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
lambda_parsed_arn = parse_arn(self._lambda_qualified_arn)
|
|
107
|
+
lambda_account_id = lambda_parsed_arn["account"]
|
|
108
|
+
lambda_region_name = lambda_parsed_arn["region"]
|
|
109
|
+
_, lambda_function_name, lambda_function_qualifier = lambda_parsed_arn[
|
|
110
|
+
"resource"
|
|
111
|
+
].split(":")
|
|
112
|
+
function_version = get_function_version(
|
|
113
|
+
function_name=lambda_function_name,
|
|
114
|
+
qualifier=lambda_function_qualifier,
|
|
115
|
+
account_id=lambda_account_id,
|
|
116
|
+
region=lambda_region_name,
|
|
117
|
+
)
|
|
118
|
+
except ResourceNotFoundException:
|
|
119
|
+
# The lambda function has not being created yet.
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self._debug_execution_environment = DebugEnabledExecutionEnvironment(
|
|
123
|
+
function_version=function_version,
|
|
124
|
+
lambda_function_debug_config=self.lambda_function_debug_config,
|
|
125
|
+
on_timeout=self._on_execution_environment_timeout,
|
|
126
|
+
)
|
|
127
|
+
# FIXME: this log should take place after RuntimeStatus.READY, however the debug-enabled
|
|
128
|
+
# docker container will not notify LS about it starting up until the user has
|
|
129
|
+
# connected a debug client. Future work should resolve this notification issue.
|
|
130
|
+
LOG.info(
|
|
131
|
+
"LDM is ready for debugger connections for '%s' on port %i.",
|
|
132
|
+
self.lambda_function_debug_config.qualified_lambda_arn,
|
|
133
|
+
self.lambda_function_debug_config.port,
|
|
134
|
+
)
|
|
135
|
+
self._debug_execution_environment.start()
|
|
136
|
+
if self._debug_execution_environment.status != RuntimeStatus.READY:
|
|
137
|
+
LOG.error(
|
|
138
|
+
"LDM could not create a debug environment for '%s'", self._lambda_qualified_arn
|
|
139
|
+
)
|
|
140
|
+
self._debug_execution_environment = None
|
|
141
|
+
|
|
142
|
+
def stop_debug_enabled_execution_environment(self):
|
|
143
|
+
with self._mutex:
|
|
144
|
+
if environment := self._debug_execution_environment:
|
|
145
|
+
environment.stop()
|
|
146
|
+
self._debug_execution_environment = None
|
|
147
|
+
|
|
148
|
+
def get_execution_environment(self) -> DebugEnabledExecutionEnvironment:
|
|
149
|
+
# TODO: add support for concurrent invokes, such as invoke object queuing, new container spinup
|
|
150
|
+
with self._mutex:
|
|
151
|
+
# TODO: move this start-up logic to lambda function creation.
|
|
152
|
+
self.start_debug_enabled_execution_environment()
|
|
153
|
+
try:
|
|
154
|
+
self._debug_execution_environment.reserve()
|
|
155
|
+
return self._debug_execution_environment
|
|
156
|
+
except InvalidStatusException:
|
|
157
|
+
LOG.warning(
|
|
158
|
+
"Concurrent lambda invocations disabled for '%s' by Lambda Debug Mode",
|
|
159
|
+
self._lambda_qualified_arn,
|
|
160
|
+
)
|
|
161
|
+
raise TooManyRequestsException(
|
|
162
|
+
"Rate Exceeded.",
|
|
163
|
+
Reason="SingleLeaseEnforcement",
|
|
164
|
+
Type="User",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _on_execution_environment_timeout(
|
|
168
|
+
self, version_manager_id: str, environment_id: str
|
|
169
|
+
) -> None:
|
|
170
|
+
# This function is run by the ExecutionEnvironment when the
|
|
171
|
+
# release of on-demand container times-out whilst waiting for
|
|
172
|
+
# invokes. However, DebugEnabledExecutionEnvironment are
|
|
173
|
+
# provisioned-concurrency ExecutionEnvironments.
|
|
174
|
+
LOG.warning(
|
|
175
|
+
"Lambda Debug Mode function '%s' timed out ('%s', '%s')",
|
|
176
|
+
self._lambda_qualified_arn,
|
|
177
|
+
version_manager_id,
|
|
178
|
+
environment_id,
|
|
179
|
+
)
|
|
180
|
+
self.stop_debug_enabled_execution_environment()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class LDMConfigFileWatch:
|
|
184
|
+
_mutex: Final[RLock]
|
|
185
|
+
_stop_event: Final[Event]
|
|
186
|
+
_watch_thread: Final[Thread]
|
|
187
|
+
|
|
188
|
+
def __init__(self):
|
|
189
|
+
self._mutex = RLock()
|
|
190
|
+
self._stop_event = Event()
|
|
191
|
+
self._watch_thread = Thread(
|
|
192
|
+
target=self._watch_logic, args=(), daemon=True, name="LDMConfigFileWatch"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def start(self):
|
|
196
|
+
with self._mutex:
|
|
197
|
+
self._stop_event.clear()
|
|
198
|
+
if not self._watch_thread.is_alive():
|
|
199
|
+
self._watch_thread.start()
|
|
200
|
+
|
|
201
|
+
def stop(self):
|
|
202
|
+
with self._mutex:
|
|
203
|
+
self._stop_event.set()
|
|
204
|
+
|
|
205
|
+
def _watch_logic(self) -> None:
|
|
206
|
+
if not LAMBDA_DEBUG_MODE_CONFIG_PATH:
|
|
207
|
+
LOG.info("LDM: no definitions for LAMBDA_DEBUG_MODE_CONFIG_PATH")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# TODO: consider relying on system calls (watchdog lib for cross-platform support)
|
|
211
|
+
# instead of monitoring last modified dates.
|
|
212
|
+
# Run the first load and signal as initialised.
|
|
213
|
+
epoch_last_loaded: int = self._config_file_epoch_last_modified_or_now()
|
|
214
|
+
self._update_ldm_from_ldm_config_file()
|
|
215
|
+
|
|
216
|
+
# Monitor for file changes whilst the application is running.
|
|
217
|
+
while not self._stop_event.is_set():
|
|
218
|
+
time.sleep(1)
|
|
219
|
+
epoch_last_modified = self._config_file_epoch_last_modified_or_now()
|
|
220
|
+
if epoch_last_modified > epoch_last_loaded:
|
|
221
|
+
epoch_last_loaded = epoch_last_modified
|
|
222
|
+
self._update_ldm_from_ldm_config_file()
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _update_ldm_from_ldm_config_file() -> None:
|
|
226
|
+
yaml_configuration_string = None
|
|
227
|
+
try:
|
|
228
|
+
with open(LAMBDA_DEBUG_MODE_CONFIG_PATH, "r") as df:
|
|
229
|
+
yaml_configuration_string = df.read()
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
LOG.error(
|
|
232
|
+
"LDM: The file lambda debug config file '%s' was not found.",
|
|
233
|
+
LAMBDA_DEBUG_MODE_CONFIG_PATH,
|
|
234
|
+
)
|
|
235
|
+
except IsADirectoryError:
|
|
236
|
+
LOG.error(
|
|
237
|
+
"LDM: Expected a lambda debug config file but found a directory at '%s'.",
|
|
238
|
+
LAMBDA_DEBUG_MODE_CONFIG_PATH,
|
|
239
|
+
)
|
|
240
|
+
except PermissionError:
|
|
241
|
+
LOG.error(
|
|
242
|
+
"LDM: Permission denied while trying to read the lambda debug config file '%s'.",
|
|
243
|
+
LAMBDA_DEBUG_MODE_CONFIG_PATH,
|
|
244
|
+
)
|
|
245
|
+
except Exception as ex:
|
|
246
|
+
LOG.error(
|
|
247
|
+
"LDM: An unexpected error occurred while reading lambda debug config '%s': '%s'",
|
|
248
|
+
LAMBDA_DEBUG_MODE_CONFIG_PATH,
|
|
249
|
+
ex,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if not yaml_configuration_string:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
config = parse_ldm_config(yaml_configuration_string)
|
|
256
|
+
if config is not None:
|
|
257
|
+
LDM.remove_all_configurations()
|
|
258
|
+
for qualified_lambda_arn, ldm_config in config.functions.items():
|
|
259
|
+
LDM.add_configuration(
|
|
260
|
+
config=LambdaFunctionDebugConfig(
|
|
261
|
+
qualified_lambda_arn=qualified_lambda_arn,
|
|
262
|
+
port=ldm_config.debug_port,
|
|
263
|
+
enforce_timeouts=ldm_config.enforce_timeouts,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
LDM.enable_configuration(qualified_lambda_arn=qualified_lambda_arn)
|
|
267
|
+
LOG.info(
|
|
268
|
+
"LDM is now enforcing the latest configuration from the LDM configuration file"
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
LOG.warning(
|
|
272
|
+
"LDM could not load the latest Lambda debug mode configuration "
|
|
273
|
+
"due to an error; check logs for more details."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _config_file_epoch_last_modified_or_now() -> int:
|
|
278
|
+
try:
|
|
279
|
+
modified_time = os.path.getmtime(LAMBDA_DEBUG_MODE_CONFIG_PATH)
|
|
280
|
+
return int(modified_time)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
LOG.warning("LDM could not access the configuration file: %s", e)
|
|
283
|
+
epoch_now = int(time.time())
|
|
284
|
+
return epoch_now
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class LambdaDebugMode:
|
|
288
|
+
_mutex: Final[RLock]
|
|
289
|
+
_is_enabled: bool
|
|
290
|
+
_debug_targets: Final[dict[str, LambdaDebugTarget]]
|
|
291
|
+
_config_file_watch: Final[Optional[LDMConfigFileWatch]]
|
|
292
|
+
|
|
293
|
+
def __init__(self):
|
|
294
|
+
self._mutex = RLock()
|
|
295
|
+
self._is_enabled = bool(LAMBDA_DEBUG_MODE)
|
|
296
|
+
self._debug_targets = dict()
|
|
297
|
+
self._config_file_watch = LDMConfigFileWatch() if LAMBDA_DEBUG_MODE_CONFIG_PATH else None
|
|
298
|
+
if self._is_enabled:
|
|
299
|
+
self.start_debug_mode()
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
@singleton_factory
|
|
303
|
+
def get() -> LambdaDebugMode:
|
|
304
|
+
"""Returns a singleton instance of the Lambda Debug Mode session."""
|
|
305
|
+
return LambdaDebugMode()
|
|
306
|
+
|
|
307
|
+
def start_debug_mode(self) -> None:
|
|
308
|
+
with self._mutex:
|
|
309
|
+
self._is_enabled = True
|
|
310
|
+
if self._config_file_watch:
|
|
311
|
+
self._config_file_watch.start()
|
|
312
|
+
|
|
313
|
+
def stop_debug_mode(self) -> None:
|
|
314
|
+
with self._mutex:
|
|
315
|
+
self._is_enabled = False
|
|
316
|
+
if self._config_file_watch:
|
|
317
|
+
self._config_file_watch.stop()
|
|
318
|
+
self.remove_all_configurations()
|
|
319
|
+
|
|
320
|
+
def is_enabled(self) -> bool:
|
|
321
|
+
return self._is_enabled
|
|
322
|
+
|
|
323
|
+
def add_configuration(self, config: LambdaFunctionDebugConfig) -> None:
|
|
324
|
+
with self._mutex:
|
|
325
|
+
if not self._is_enabled:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
arn = config.qualified_lambda_arn
|
|
329
|
+
if existing_target := self._debug_targets.get(arn):
|
|
330
|
+
existing_target.stop_debug_enabled_execution_environment()
|
|
331
|
+
|
|
332
|
+
target = LambdaDebugTarget(lambda_function_debug_config=config)
|
|
333
|
+
self._debug_targets[arn] = target
|
|
334
|
+
|
|
335
|
+
def enable_configuration(self, qualified_lambda_arn: Arn) -> None:
|
|
336
|
+
with self._mutex:
|
|
337
|
+
if not self._is_enabled:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
if target := self._debug_targets.get(qualified_lambda_arn):
|
|
341
|
+
threading.Thread(
|
|
342
|
+
target=target.start_debug_enabled_execution_environment,
|
|
343
|
+
args=(),
|
|
344
|
+
name=f"LambdaDebugTarget-start_debug_enabled_execution_environment-{qualified_lambda_arn}",
|
|
345
|
+
daemon=True,
|
|
346
|
+
).start()
|
|
347
|
+
|
|
348
|
+
def remove_configuration(self, qualified_lambda_arn: Arn) -> None:
|
|
349
|
+
with self._mutex:
|
|
350
|
+
if not self._is_enabled:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if target := self._debug_targets.pop(qualified_lambda_arn, None):
|
|
354
|
+
target.stop_debug_enabled_execution_environment()
|
|
355
|
+
|
|
356
|
+
def remove_all_configurations(self) -> None:
|
|
357
|
+
with self._mutex:
|
|
358
|
+
for target in self._debug_targets.values():
|
|
359
|
+
target.stop_debug_enabled_execution_environment()
|
|
360
|
+
self._debug_targets.clear()
|
|
361
|
+
|
|
362
|
+
def get_execution_environment(
|
|
363
|
+
self, qualified_lambda_arn: Arn, user_agent: Optional[str]
|
|
364
|
+
) -> Optional[DebugEnabledExecutionEnvironment]:
|
|
365
|
+
if not self._is_enabled:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
if target := self._debug_targets.get(qualified_lambda_arn):
|
|
369
|
+
target_user_agent = target.lambda_function_debug_config.user_agent
|
|
370
|
+
if target_user_agent is None or target_user_agent == user_agent:
|
|
371
|
+
return target.get_execution_environment()
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
LDM: Final[LambdaDebugMode] = LambdaDebugMode.get()
|