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.
Files changed (30) hide show
  1. localstack/services/lambda_/event_source_mapping/esm_worker_factory.py +5 -5
  2. localstack/services/lambda_/invocation/assignment.py +1 -23
  3. localstack/services/lambda_/invocation/counting_service.py +0 -21
  4. localstack/services/lambda_/invocation/docker_runtime_executor.py +5 -4
  5. localstack/services/lambda_/invocation/execution_environment.py +1 -18
  6. localstack/services/lambda_/invocation/executor_endpoint.py +4 -4
  7. localstack/services/lambda_/invocation/lambda_models.py +1 -0
  8. localstack/services/lambda_/invocation/lambda_service.py +3 -0
  9. localstack/services/lambda_/invocation/version_manager.py +25 -0
  10. localstack/services/lambda_/lambda_debug_mode/ldm.py +375 -0
  11. localstack/services/lambda_/lambda_debug_mode/ldm_config_file.py +178 -0
  12. localstack/services/lambda_/lambda_debug_mode/ldm_types.py +11 -0
  13. localstack/services/lambda_/lambda_debug_mode/ldm_utils.py +43 -0
  14. localstack/services/lambda_/provider.py +14 -14
  15. localstack/version.py +2 -2
  16. {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/METADATA +1 -1
  17. {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/RECORD +26 -25
  18. localstack_core-4.6.1.dev31.dist-info/plux.json +1 -0
  19. localstack/utils/lambda_debug_mode/lambda_debug_mode.py +0 -41
  20. localstack/utils/lambda_debug_mode/lambda_debug_mode_config.py +0 -230
  21. localstack/utils/lambda_debug_mode/lambda_debug_mode_session.py +0 -175
  22. localstack_core-4.6.1.dev30.dist-info/plux.json +0 -1
  23. /localstack/{utils → services/lambda_}/lambda_debug_mode/__init__.py +0 -0
  24. {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack +0 -0
  25. {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack-supervisor +0 -0
  26. {localstack_core-4.6.1.dev30.data → localstack_core-4.6.1.dev31.data}/scripts/localstack.bat +0 -0
  27. {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/WHEEL +0 -0
  28. {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/entry_points.txt +0 -0
  29. {localstack_core-4.6.1.dev30.dist-info → localstack_core-4.6.1.dev31.dist-info}/licenses/LICENSE.txt +0 -0
  30. {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.utils.aws.arns import parse_arn
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
- is_lambda_debug_mode,
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 is_lambda_debug_mode():
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
- if is_lambda_debug_timeout_enabled_for(lambda_arn=function_version.qualified_arn):
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._get_execution_timeout_seconds(),
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.utils.backoff import ExponentialBackoff
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
- is_lambda_debug_mode,
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 is_lambda_debug_mode():
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:
@@ -66,6 +66,7 @@ class Invocation:
66
66
  # = invocation_id
67
67
  request_id: str
68
68
  trace_context: dict
69
+ user_agent: Optional[str] = None
69
70
 
70
71
 
71
72
  InitializationType = Literal["on-demand", "provisioned-concurrency"]
@@ -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()