Skip to content

Commit d46c1be

Browse files
committed
feat: support for local Event invocation type
1 parent cde14de commit d46c1be

5 files changed

Lines changed: 387 additions & 49 deletions

File tree

samcli/commands/local/lib/local_lambda.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,43 @@ def __init__(
9696
self.container_host_interface = container_host_interface
9797
self.extra_hosts = extra_hosts
9898

99+
def get_function(self, function_identifier: str) -> Function:
100+
"""
101+
Get a Lambda function by identifier, raising FunctionNotFound if not found.
102+
103+
Parameters
104+
----------
105+
function_identifier : str
106+
Identifier of the Lambda function, it can be logicalID, function name or full path
107+
108+
Returns
109+
-------
110+
Function
111+
The Lambda function configuration
112+
113+
Raises
114+
------
115+
InvalidFunctionNameException
116+
When the function identifier doesn't match AWS Lambda's validation pattern
117+
FunctionNotFound
118+
When we cannot find a function with the given identifier
119+
"""
120+
# Normalize function identifier from ARN if provided
121+
normalized_function_identifier = normalize_sam_function_identifier(function_identifier)
122+
123+
# Generate the correct configuration based on given inputs
124+
function = self.provider.get(normalized_function_identifier)
125+
126+
if not function:
127+
all_function_full_paths = [f.full_path for f in self.provider.get_all()]
128+
available_function_message = "{} not found. Possible options in your template: {}".format(
129+
function_identifier, all_function_full_paths
130+
)
131+
LOG.info(available_function_message)
132+
raise FunctionNotFound("Unable to find a Function with name '{}'".format(function_identifier))
133+
134+
return function
135+
99136
def invoke(
100137
self,
101138
function_identifier: str,
@@ -138,19 +175,8 @@ def invoke(
138175
FunctionNotfound
139176
When we cannot find a function with the given name
140177
"""
141-
# Normalize function identifier from ARN if provided
142-
normalized_function_identifier = normalize_sam_function_identifier(function_identifier)
143-
144-
# Generate the correct configuration based on given inputs
145-
function = self.provider.get(normalized_function_identifier)
146-
147-
if not function:
148-
all_function_full_paths = [f.full_path for f in self.provider.get_all()]
149-
available_function_message = "{} not found. Possible options in your template: {}".format(
150-
function_identifier, all_function_full_paths
151-
)
152-
LOG.info(available_function_message)
153-
raise FunctionNotFound("Unable to find a Function with name '{}'".format(function_identifier))
178+
# Get the function configuration
179+
function = self.get_function(function_identifier)
154180

155181
LOG.debug("Found one Lambda function with name '%s'", function_identifier)
156182
if function.packagetype == ZIP:

samcli/local/lambda_service/local_lambda_http_service.py

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io
44
import json
55
import logging
6+
from concurrent.futures import ThreadPoolExecutor
67
from datetime import datetime
78
from urllib.parse import unquote
89

@@ -11,6 +12,7 @@
1112

1213
from samcli.commands.local.cli_common.durable_context import DurableContext
1314
from samcli.commands.local.lib.exceptions import TenantIdValidationError, UnsupportedInlineCodeError
15+
from samcli.lib.utils.invocation_type import EVENT
1416
from samcli.lib.utils.name_utils import InvalidFunctionNameException, normalize_sam_function_identifier
1517
from samcli.lib.utils.stream_writer import StreamWriter
1618
from samcli.local.docker.exceptions import DockerContainerCreationFailedException
@@ -67,6 +69,7 @@ def __init__(self, lambda_runner, port, host, stderr=None, ssl_context=None):
6769
super().__init__(lambda_runner.is_debugging(), port=port, host=host, ssl_context=ssl_context)
6870
self.lambda_runner = lambda_runner
6971
self.stderr = stderr
72+
self.executor = ThreadPoolExecutor()
7073

7174
def create(self):
7275
"""
@@ -240,32 +243,36 @@ def _invoke_request_handler(self, function_name):
240243
# Extract durable execution name from headers
241244
durable_execution_name = flask_request.headers.get("X-Amz-Durable-Execution-Name")
242245

243-
stdout_stream_string = io.StringIO()
244-
stdout_stream_bytes = io.BytesIO()
245-
stdout_stream_writer = StreamWriter(stdout_stream_string, stdout_stream_bytes, auto_flush=True)
246+
arguments = {
247+
"function_name": function_name,
248+
"request_data": request_data,
249+
"invocation_type": invocation_type,
250+
"durable_execution_name": durable_execution_name,
251+
"tenant_id": tenant_id,
252+
}
253+
254+
headers = {"Content-Type": "application/json"}
255+
256+
if invocation_type == EVENT:
257+
# Validate function exists before submitting async task
258+
if validation_error := self._validate_function_for_invocation(function_name):
259+
return validation_error
260+
261+
self.executor.submit(self._invoke_async_lambda, **arguments)
262+
return self.service_response("", headers, 202)
246263

247264
try:
248-
# Normalize function name from ARN if provided
249-
normalized_function_name = normalize_sam_function_identifier(function_name)
250-
251-
invoke_headers = self.lambda_runner.invoke(
252-
normalized_function_name,
253-
request_data,
254-
invocation_type=invocation_type,
255-
durable_execution_name=durable_execution_name,
256-
tenant_id=tenant_id,
257-
stdout=stdout_stream_writer,
258-
stderr=self.stderr,
259-
)
265+
invoke_headers, stdout_stream_string, stdout_stream_bytes = self._invoke_lambda(**arguments)
260266
except (InvalidFunctionNameException, TenantIdValidationError) as e:
261267
LOG.error("Validation error: %s", str(e))
262268
return LambdaErrorResponses.validation_exception(str(e))
263269
except UnsupportedInvocationType as e:
264-
LOG.warning("invocation-type: %s is not supported. RequestResponse is only supported.", invocation_type)
270+
LOG.warning(
271+
"invocation-type: %s is not supported. Event and RequestResponse are only supported.", invocation_type
272+
)
265273
return LambdaErrorResponses.not_implemented_locally(str(e))
266274
except FunctionNotFound:
267-
LOG.debug("%s was not found to invoke.", normalized_function_name)
268-
return LambdaErrorResponses.resource_not_found(normalized_function_name)
275+
return self._handle_function_not_found(function_name)
269276
except UnsupportedInlineCodeError:
270277
return LambdaErrorResponses.not_implemented_locally(
271278
"Inline code is not supported for sam local commands. Please write your code in a separate file."
@@ -278,20 +285,87 @@ def _invoke_request_handler(self, function_name):
278285
)
279286

280287
# Prepare headers
281-
headers = {"Content-Type": "application/json"}
282288
if invoke_headers and isinstance(invoke_headers, dict):
283289
headers.update(invoke_headers)
284290

285291
if is_lambda_user_error_response:
286292
headers["x-amz-function-error"] = "Unhandled"
287-
return self.service_response(lambda_response, headers, 200)
288-
289-
# For async invocations (Event type), return 202
290-
if invocation_type == "Event":
291-
return self.service_response("", headers, 202)
292293

293294
return self.service_response(lambda_response, headers, 200)
294295

296+
def _validate_function_for_invocation(self, function_name):
297+
"""
298+
Validates that a function exists and can be invoked.
299+
300+
Parameters
301+
----------
302+
function_name : str
303+
Name or ARN of the function to validate
304+
305+
Returns
306+
-------
307+
Flask.Response or None
308+
Error response if validation fails, None if validation succeeds
309+
"""
310+
try:
311+
self.lambda_runner.get_function(function_name)
312+
return None
313+
except FunctionNotFound:
314+
return self._handle_function_not_found(function_name)
315+
except InvalidFunctionNameException as e:
316+
LOG.error("Validation error: %s", str(e))
317+
return LambdaErrorResponses.validation_exception(str(e))
318+
319+
def _handle_function_not_found(self, function_name):
320+
"""
321+
Handles FunctionNotFound exception by returning appropriate error response.
322+
323+
Parameters
324+
----------
325+
function_name : str
326+
Name or ARN of the function that was not found
327+
328+
Returns
329+
-------
330+
Flask.Response
331+
Error response for function not found
332+
"""
333+
normalized_function_name = normalize_sam_function_identifier(function_name)
334+
LOG.debug("%s was not found to invoke.", normalized_function_name)
335+
return LambdaErrorResponses.resource_not_found(normalized_function_name)
336+
337+
def _invoke_async_lambda(self, function_name, **kwargs):
338+
"""
339+
Wrapper for _invoke_lambda that runs in an async context (Event invocation type)
340+
"""
341+
try:
342+
self._invoke_lambda(function_name=function_name, **kwargs)
343+
except Exception as e:
344+
LOG.error("Async invocation failed for function %s: %s", function_name, str(e), exc_info=True)
345+
346+
def _invoke_lambda(self, function_name, request_data, invocation_type, durable_execution_name, tenant_id):
347+
"""
348+
Invokes a Lambda function and returns the result
349+
"""
350+
351+
stdout_stream_string = io.StringIO()
352+
stdout_stream_bytes = io.BytesIO()
353+
stdout_stream_writer = StreamWriter(stdout_stream_string, stdout_stream_bytes, auto_flush=True)
354+
355+
normalized_function_name = normalize_sam_function_identifier(function_name)
356+
357+
invoke_headers = self.lambda_runner.invoke(
358+
normalized_function_name,
359+
request_data,
360+
invocation_type=invocation_type,
361+
durable_execution_name=durable_execution_name,
362+
tenant_id=tenant_id,
363+
stdout=stdout_stream_writer,
364+
stderr=self.stderr,
365+
)
366+
367+
return invoke_headers, stdout_stream_string, stdout_stream_bytes
368+
295369
def _get_durable_execution_handler(self, durable_execution_arn):
296370
"""
297371
Handler for GET /2025-12-01/durable-executions/{DurableExecutionArn}

samcli/local/lambdafn/runtime.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from samcli.lib.telemetry.metric import capture_parameter
1515
from samcli.lib.utils.file_observer import LambdaFunctionObserver
16+
from samcli.lib.utils.invocation_type import EVENT, REQUEST_RESPONSE
1617
from samcli.lib.utils.packagetype import ZIP
1718
from samcli.local.docker.container import Container, ContainerContext
1819
from samcli.local.docker.container_analyzer import ContainerAnalyzer
@@ -305,9 +306,10 @@ def invoke(
305306
)
306307
else:
307308
# Only RequestResponse supported for regular Lambda functions
308-
if invocation_type != "RequestResponse":
309+
if invocation_type not in [EVENT, REQUEST_RESPONSE]:
309310
raise UnsupportedInvocationType(
310-
f"invocation-type: {invocation_type} is not supported. RequestResponse is only supported."
311+
f"invocation-type: {invocation_type} is not supported. "
312+
"Event and RequestResponse are only supported."
311313
)
312314

313315
# The container handles concurrency control internally via its semaphore.

0 commit comments

Comments
 (0)