Skip to content

Commit f3a9a9c

Browse files
committed
feat: Add support for LMI to python RIC
1 parent 3f43f4d commit f3a9a9c

17 files changed

+832
-87
lines changed

awslambdaric/__main__.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,31 @@
22
Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
"""
44

5-
import os
65
import sys
76

7+
from .lambda_config import LambdaConfigProvider
8+
from .lambda_runtime_client import LambdaRuntimeClient
9+
from .lambda_elevator_utils import ElevatorRunner
810
from . import bootstrap
911

1012

1113
def main(args):
12-
app_root = os.getcwd()
13-
14-
try:
15-
handler = args[1]
16-
except IndexError:
17-
raise ValueError("Handler not set")
18-
19-
lambda_runtime_api_addr = os.environ["AWS_LAMBDA_RUNTIME_API"]
20-
21-
bootstrap.run(app_root, handler, lambda_runtime_api_addr)
14+
config = LambdaConfigProvider(args)
15+
handler = config.handler
16+
api_addr = config.api_address
17+
use_thread = config.use_thread_polling
18+
19+
if config.is_elevator:
20+
# Elevator mode: redirect fork, stdout/strerr and run
21+
max_conc = int(config.max_concurrency)
22+
socket_path = config.elevator_socket_path
23+
ElevatorRunner.run_concurrent(
24+
handler, api_addr, use_thread, socket_path, max_conc
25+
)
26+
else:
27+
# Standard Lambda mode: single call
28+
client = LambdaRuntimeClient(api_addr, use_thread)
29+
bootstrap.run(handler, client)
2230

2331

2432
if __name__ == "__main__":

awslambdaric/bootstrap.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -477,19 +477,11 @@ def _setup_logging(log_format, log_level, log_sink):
477477
logger.addHandler(logger_handler)
478478

479479

480-
def run(app_root, handler, lambda_runtime_api_addr):
480+
def run(handler, lambda_runtime_client):
481481
sys.stdout = Unbuffered(sys.stdout)
482482
sys.stderr = Unbuffered(sys.stderr)
483483

484-
use_thread_for_polling_next = os.environ.get("AWS_EXECUTION_ENV") in {
485-
"AWS_Lambda_python3.12",
486-
"AWS_Lambda_python3.13",
487-
}
488-
489484
with create_log_sink() as log_sink:
490-
lambda_runtime_client = LambdaRuntimeClient(
491-
lambda_runtime_api_addr, use_thread_for_polling_next
492-
)
493485
error_result = None
494486

495487
try:

awslambdaric/lambda_config.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
"""
4+
5+
import os
6+
7+
8+
class LambdaConfigProvider:
9+
SUPPORTED_THREADPOLLING_ENVS = {
10+
"AWS_Lambda_python3.12",
11+
"AWS_Lambda_python3.13",
12+
"AWS_Lambda_python3.14",
13+
}
14+
SOCKET_PATH_ENV = "_LAMBDA_TELEMETRY_LOG_FD_PROVIDER_SOCKET"
15+
AWS_LAMBDA_RUNTIME_API = "AWS_LAMBDA_RUNTIME_API"
16+
AWS_LAMBDA_MAX_CONCURRENCY = "AWS_LAMBDA_MAX_CONCURRENCY"
17+
AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV"
18+
19+
def __init__(self, args, environ=None):
20+
self._environ = environ if environ is not None else os.environ
21+
self._handler = self._parse_handler(args)
22+
self._api_address = self._parse_api_address()
23+
self._max_concurrency = self._parse_concurrency()
24+
self._use_thread_polling = self._parse_thread_polling()
25+
self._elevator_socket_path = self._parse_elevator_socket_path()
26+
27+
def _parse_handler(self, args):
28+
try:
29+
return args[1]
30+
except IndexError:
31+
raise ValueError("Handler not set")
32+
33+
def _parse_api_address(self):
34+
return self._environ[self.AWS_LAMBDA_RUNTIME_API]
35+
36+
def _parse_concurrency(self):
37+
return self._environ.get(self.AWS_LAMBDA_MAX_CONCURRENCY)
38+
39+
def _parse_thread_polling(self):
40+
return (
41+
self._environ.get(self.AWS_EXECUTION_ENV)
42+
in self.SUPPORTED_THREADPOLLING_ENVS
43+
)
44+
45+
def _parse_elevator_socket_path(self):
46+
return self._environ.get(self.SOCKET_PATH_ENV)
47+
48+
@property
49+
def handler(self):
50+
return self._handler
51+
52+
@property
53+
def api_address(self):
54+
return self._api_address
55+
56+
@property
57+
def max_concurrency(self):
58+
return self._max_concurrency
59+
60+
@property
61+
def use_thread_polling(self):
62+
return self._use_thread_polling
63+
64+
@property
65+
def is_elevator(self):
66+
return self._max_concurrency is not None
67+
68+
@property
69+
def elevator_socket_path(self):
70+
return self._elevator_socket_path
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
"""
4+
5+
import os
6+
import sys
7+
import socket
8+
import multiprocessing
9+
10+
from . import bootstrap
11+
from .lambda_runtime_client import LambdaElevatorRuntimeClient
12+
13+
14+
class ElevatorRunner:
15+
@staticmethod
16+
def _redirect_stream_to_fd(stream_fd: int, socket_path: str):
17+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
18+
s.connect(socket_path)
19+
os.dup2(s.fileno(), stream_fd)
20+
21+
@classmethod
22+
def _redirect_output(cls, socket_path: str):
23+
for std_fd in (sys.stdout.fileno(), sys.stderr.fileno()):
24+
cls._redirect_stream_to_fd(std_fd, socket_path)
25+
26+
@classmethod
27+
def run_single(
28+
cls, handler: str, api_addr: str, use_thread: bool, socket_path: str
29+
):
30+
if socket_path:
31+
cls._redirect_output(socket_path)
32+
client = LambdaElevatorRuntimeClient(api_addr, use_thread)
33+
bootstrap.run(handler, client)
34+
35+
@classmethod
36+
def run_concurrent(
37+
cls,
38+
handler: str,
39+
api_addr: str,
40+
use_thread: bool,
41+
socket_path: str,
42+
max_concurrency: int,
43+
):
44+
processes = []
45+
for _ in range(max_concurrency):
46+
p = multiprocessing.Process(
47+
target=cls.run_single,
48+
args=(handler, api_addr, use_thread, socket_path),
49+
)
50+
p.start()
51+
processes.append(p)
52+
for p in processes:
53+
p.join()

awslambdaric/lambda_runtime_client.py

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
from awslambdaric import __version__
77
from .lambda_runtime_exception import FaultException
88
from .lambda_runtime_marshaller import to_json
9+
import logging
10+
import time
911

1012
ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"
13+
# Retry config constants
14+
DEFAULT_RETRY_MAX_ATTEMPTS = 5
15+
DEFAULT_RETRY_INITIAL_DELAY = 0.1 # seconds
16+
DEFAULT_RETRY_BACKOFF_FACTOR = 2.0
1117

1218

1319
def _user_agent():
@@ -46,13 +52,17 @@ def __init__(self, endpoint, response_code, response_body):
4652
)
4753

4854

49-
class LambdaRuntimeClient(object):
55+
class BaseLambdaRuntimeClient(object):
5056
marshaller = LambdaMarshaller()
5157
"""marshaller is a class attribute that determines the unmarshalling and marshalling logic of a function's event
5258
and response. It allows for function authors to override the the default implementation, LambdaMarshaller which
5359
unmarshals and marshals JSON, to an instance of a class that implements the same interface."""
5460

55-
def __init__(self, lambda_runtime_address, use_thread_for_polling_next=False):
61+
def __init__(
62+
self,
63+
lambda_runtime_address,
64+
use_thread_for_polling_next=False,
65+
):
5666
self.lambda_runtime_address = lambda_runtime_address
5767
self.use_thread_for_polling_next = use_thread_for_polling_next
5868
if self.use_thread_for_polling_next:
@@ -94,9 +104,16 @@ def post_init_error(self, error_response_data, error_type_override=None):
94104
else error_response_data["errorType"]
95105
)
96106
}
97-
self.call_rapid(
98-
"POST", endpoint, http.HTTPStatus.ACCEPTED, error_response_data, headers
99-
)
107+
try:
108+
self.call_rapid(
109+
"POST", endpoint, http.HTTPStatus.ACCEPTED, error_response_data, headers
110+
)
111+
except Exception as e:
112+
self.handle_init_error(e)
113+
114+
def handle_init_error(self, exc):
115+
"""Override in subclasses to customize init error handling."""
116+
raise NotImplementedError
100117

101118
def restore_next(self):
102119
import http
@@ -113,14 +130,24 @@ def report_restore_error(self, restore_error_data):
113130
"POST", endpoint, http.HTTPStatus.ACCEPTED, restore_error_data, headers
114131
)
115132

133+
def handle_exception(self, exc, func_to_retry=None, use_backoff=False):
134+
"""Override in subclasses to customize error handling."""
135+
raise NotImplementedError
136+
137+
def _get_next(self):
138+
try:
139+
return runtime_client.next()
140+
except Exception as e:
141+
return self.handle_exception(e, runtime_client.next, True)
142+
116143
def wait_next_invocation(self):
117144
# Calling runtime_client.next() from a separate thread unblocks the main thread,
118145
# which can then process signals.
119146
if self.use_thread_for_polling_next:
120147
try:
121148
# TPE class is supposed to be registered at construction time and be ready to use.
122149
with self.ThreadPoolExecutor(max_workers=1) as executor:
123-
future = executor.submit(runtime_client.next)
150+
future = executor.submit(self._get_next)
124151
response_body, headers = future.result()
125152
except Exception as e:
126153
raise FaultException(
@@ -145,17 +172,66 @@ def wait_next_invocation(self):
145172
def post_invocation_result(
146173
self, invoke_id, result_data, content_type="application/json"
147174
):
148-
runtime_client.post_invocation_result(
149-
invoke_id,
150-
(
151-
result_data
152-
if isinstance(result_data, bytes)
153-
else result_data.encode("utf-8")
154-
),
155-
content_type,
156-
)
175+
try:
176+
runtime_client.post_invocation_result(
177+
invoke_id,
178+
(
179+
result_data
180+
if isinstance(result_data, bytes)
181+
else result_data.encode("utf-8")
182+
),
183+
content_type,
184+
)
185+
except Exception as e:
186+
self.handle_exception(e)
157187

158188
def post_invocation_error(self, invoke_id, error_response_data, xray_fault):
159-
max_header_size = 1024 * 1024 # 1MiB
160-
xray_fault = xray_fault if len(xray_fault.encode()) < max_header_size else ""
161-
runtime_client.post_error(invoke_id, error_response_data, xray_fault)
189+
try:
190+
max_header_size = 1024 * 1024
191+
xray_fault = (
192+
xray_fault if len(xray_fault.encode()) < max_header_size else ""
193+
)
194+
runtime_client.post_error(invoke_id, error_response_data, xray_fault)
195+
except Exception as e:
196+
self.handle_exception(e)
197+
198+
199+
class LambdaRuntimeClient(BaseLambdaRuntimeClient):
200+
def handle_exception(self, exc, func_to_retry=None, use_backoff=False):
201+
raise exc
202+
203+
def handle_init_error(self, exc):
204+
raise exc
205+
206+
207+
class LambdaElevatorRuntimeClient(BaseLambdaRuntimeClient):
208+
def _get_next_with_backoff(self, e, func_to_retry):
209+
logging.warning(f"Initial runtime_client.next() failed: {e}")
210+
delay = DEFAULT_RETRY_INITIAL_DELAY
211+
latest_exception = None
212+
for attempt in range(1, DEFAULT_RETRY_MAX_ATTEMPTS):
213+
try:
214+
logging.info(
215+
f"Retrying runtime_client.next() [attempt {attempt + 1}]..."
216+
)
217+
time.sleep(delay)
218+
return func_to_retry()
219+
except Exception as e:
220+
logging.warning(f"Attempt {attempt + 1} failed: {e}")
221+
delay *= DEFAULT_RETRY_BACKOFF_FACTOR
222+
latest_exception = e
223+
224+
raise latest_exception
225+
226+
# In elevator we don't want to raises unhandled exception and crash the worker on non-2xx responses from RAPID
227+
def handle_exception(self, exc, func_to_retry=None, use_backoff=False):
228+
if use_backoff:
229+
return self._get_next_with_backoff(exc, func_to_retry)
230+
# We retry if getting next invoke failed, but if posting response to RAPID failed we just log it and continue
231+
logging.warning(f"{exc}: This won't kill the Runtime loop")
232+
233+
def handle_init_error(self, exc):
234+
if isinstance(exc, LambdaRuntimeClientError) and exc.response_code == 403:
235+
# Suppress 403 errors from RAPID during init - indicates another runtime worker has already posted init error
236+
return
237+
raise exc

awslambdaric/lambda_runtime_marshaller.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self):
1818
if os.environ.get("AWS_EXECUTION_ENV") in {
1919
"AWS_Lambda_python3.12",
2020
"AWS_Lambda_python3.13",
21+
"AWS_Lambda_python3.14",
2122
}:
2223
super().__init__(use_decimal=False, ensure_ascii=False, allow_nan=True)
2324
else:

deps/aws-lambda-cpp-0.2.6.tar.gz

938 Bytes
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
diff --git a/src/runtime.cpp b/src/runtime.cpp
2+
index 9763282..9fe78d8 100644
3+
--- a/src/runtime.cpp
4+
+++ b/src/runtime.cpp
5+
@@ -379,7 +379,10 @@ runtime::post_outcome runtime::do_post(
6+
7+
if (!is_success(aws::http::response_code(http_response_code))) {
8+
logging::log_error(
9+
- LOG_TAG, "Failed to post handler success response. Http response code: %ld.", http_response_code);
10+
+ LOG_TAG,
11+
+ "Failed to post handler success response. Http response code: %ld. %s",
12+
+ http_response_code,
13+
+ resp.get_body().c_str());
14+
return aws::http::response_code(http_response_code);
15+
}
16+

scripts/update_deps.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ wget -c https://github.com/awslabs/aws-lambda-cpp/archive/v$AWS_LAMBDA_CPP_RELEA
3131
patch -p1 < ../patches/aws-lambda-cpp-make-the-runtime-client-user-agent-overrideable.patch && \
3232
patch -p1 < ../patches/aws-lambda-cpp-make-lto-optional.patch && \
3333
patch -p1 < ../patches/aws-lambda-cpp-add-content-type.patch && \
34-
patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch
34+
patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch && \
35+
patch -p1 < ../patches/aws-lambda-cpp-logging-error.patch
3536
)
3637

3738
## Pack again and remove the folder

0 commit comments

Comments
 (0)