Skip to content

Commit e248ac7

Browse files
committed
Update lambda handler function extraction to work with python 3.9
When running on lambda functions, the CodeGuru Profiler module should be loaded by lambda bootstrap framework instead of the customer's code. Then we load the customer's handler function by calling the bootstarp code directly. We currently handle python 3.8, 3.7 and 3.6 (with some tweeks), however for python 3.9 the lambda bootstrap module is now delegating to the [runtime interface client](https://github.com/aws/aws-lambda-python-runtime-interface-client) so our code must be changed to also work with this. This changes moves the code about finding the proper function that can load a handler function into a separate method. Unit tests are updated to cope with this.
1 parent 75fc2a2 commit e248ac7

File tree

2 files changed

+142
-55
lines changed

2 files changed

+142
-55
lines changed

codeguru_profiler_agent/aws_lambda/lambda_handler.py

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
11
import os
2-
import logging
2+
import importlib
33
from codeguru_profiler_agent.aws_lambda.profiler_decorator import with_lambda_profiler
44
from codeguru_profiler_agent.agent_metadata.aws_lambda import HANDLER_ENV_NAME_FOR_CODEGURU_KEY
55
HANDLER_ENV_NAME = "_HANDLER"
6-
logger = logging.getLogger(__name__)
76

87

98
def restore_handler_env(original_handler, env=os.environ):
109
env[HANDLER_ENV_NAME] = original_handler
1110

1211

13-
def load_handler(bootstrap_module, env=os.environ, original_handler_env_key=HANDLER_ENV_NAME_FOR_CODEGURU_KEY):
12+
def load_handler(handler_extractor, env=os.environ, original_handler_env_key=HANDLER_ENV_NAME_FOR_CODEGURU_KEY):
1413
try:
1514
original_handler_name = env.get(original_handler_env_key)
1615
if not original_handler_name:
1716
raise ValueError("Could not find module and function name from " + HANDLER_ENV_NAME_FOR_CODEGURU_KEY
1817
+ " environment variable")
1918

20-
# Delegate to the lambda code to load the customer's module.
21-
if hasattr(bootstrap_module, '_get_handler'):
22-
customer_handler_function = bootstrap_module._get_handler(original_handler_name)
23-
else:
24-
# TODO FIXME Review if the support for python 3.6 bootstrap can be improved.
25-
# This returns both a init_handler and the function, we apply the init right away as we are in init process
26-
init_handler, customer_handler_function = bootstrap_module._get_handlers(
27-
handler=original_handler_name,
28-
mode='event', # with 'event' it will return the function as is (handlerfn in the lambda code)
29-
# 'http' would return wsgi.handle_one(sockfd, ('localhost', 80), handlerfn) instead
30-
invokeid='unknown_id') # FIXME invokeid is used for error handling, need to see if we can get it
31-
init_handler()
19+
# Delegate to the lambda code to load the customer's module and function.
20+
customer_handler_function = handler_extractor(original_handler_name)
21+
3222
restore_handler_env(original_handler_name, env)
3323
return customer_handler_function
3424
except:
@@ -39,10 +29,60 @@ def load_handler(bootstrap_module, env=os.environ, original_handler_env_key=HAND
3929
raise
4030

4131

42-
# Load the customer's handler, this should be done at import time which means it is done when lambda frameworks
43-
# loads our module. We load the bootstrap module by string name so that IDE does not complain
44-
lambda_bootstrap_module = __import__("bootstrap")
45-
handler_function = load_handler(lambda_bootstrap_module)
32+
def _python36_extractor(bootstrap_module, original_handler_name):
33+
"""
34+
The lambda bootstrap code for python 3.6 was different than for later versions, instead of the _get_handler
35+
function there was a more complex _get_handlers function with more parameters
36+
"""
37+
# TODO FIXME Review if the support for python 3.6 bootstrap can be improved.
38+
# This returns both a init_handler and the function, we apply the init right away as we are in init process
39+
init_handler, customer_handler_function = bootstrap_module._get_handlers(
40+
handler=original_handler_name,
41+
mode='event', # with 'event' it will return the function as is (handlerfn in the lambda code)
42+
# 'http' would return wsgi.handle_one(sockfd, ('localhost', 80), handlerfn) instead
43+
invokeid='unknown_id') # FIXME invokeid is used for error handling, need to see if we can get it
44+
init_handler()
45+
return customer_handler_function
46+
47+
48+
def get_lambda_handler_extractor():
49+
"""
50+
This loads and returns a function from lambda or RIC source code that is able to load the customer's
51+
handler function.
52+
WARNING !! This is a bit dangerous since we are calling internal functions from other modules that we do not
53+
officially depend on. The idea is that this code should run only in a lambda function environment where we can know
54+
what is available. However if lambda developers decide to change their internal code it could impact this !
55+
"""
56+
# First try to load the lambda RIC if it is available (i.e. python 3.9)
57+
# See https://github.com/aws/aws-lambda-python-runtime-interface-client
58+
ric_bootstrap_module = _try_to_load_module("awslambdaric.bootstrap")
59+
if ric_bootstrap_module is not None and hasattr(ric_bootstrap_module, '_get_handler'):
60+
return ric_bootstrap_module._get_handler
61+
62+
# If no RIC module is available there should be a bootstrap module available
63+
# do not catch ModuleNotFoundError exceptions here as we cannot do anything if this fails.
64+
bootstrap_module = importlib.import_module("bootstrap")
65+
if hasattr(bootstrap_module, '_get_handler'):
66+
return bootstrap_module._get_handler
67+
else:
68+
return lambda handler_name: _python36_extractor(bootstrap_module, handler_name)
69+
70+
71+
def _try_to_load_module(module_name):
72+
try:
73+
return importlib.import_module(module_name)
74+
except ModuleNotFoundError:
75+
return None
76+
77+
78+
# We need to load the customer's handler function since the lambda framework loaded our function instead.
79+
# We want to delegate this work to the lambda framework so we need to find the appropriate method that does it
80+
# (depends on python versions) so we can call it.
81+
# This should be done at import time which means it is done when lambda frameworks loads our module
82+
handler_extractor = get_lambda_handler_extractor()
83+
84+
# Now load the actual customer's handler function.
85+
handler_function = load_handler(handler_extractor)
4686

4787

4888
# WARNING: Do not rename this file, this function or HANDLER_ENV_NAME_FOR_CODEGURU without changing the bootstrap script

test/unit/aws_lambda/test_lambda_handler.py

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def handler_function(event, context):
1111

1212

1313
init_handler_has_been_called = False
14+
python36_extractor_has_been_called = False
1415

1516

1617
def init_handler():
@@ -27,10 +28,18 @@ def _get_handler(self, handler):
2728
class BootstrapPython36ModuleMock:
2829
# for python3.6 version of lambda runtime bootstrap
2930
def _get_handlers(self, handler, mode, invokeid):
31+
global python36_extractor_has_been_called
32+
python36_extractor_has_been_called = True
3033
if handler == "handler_module.handler_function" and mode == "event":
3134
return init_handler, handler_function
3235

3336

37+
class RicBootstrapModuleMock:
38+
def _get_handler(self, handler):
39+
if handler == "handler_module.handler_function":
40+
return handler_function
41+
42+
3443
class TestLambdaHandler:
3544
class TestWhenLambdaHandlerModuleIsLoaded:
3645
@pytest.fixture(autouse=True)
@@ -53,48 +62,86 @@ def test_call_handler_calls_the_inner_handler(self):
5362
assert lambda_handler_module.call_handler(event="expected_event",
5463
context="expected_context") == "expected result"
5564

56-
class TestLoadModuleFunction:
57-
class TestWhenPython38LambdaBootstrapCalls:
58-
class TestWhenHandlerEnvIsSetProperly:
59-
@before
60-
def before(self):
61-
self.bootstrap = BootstrapModuleMock()
62-
self.env = {"HANDLER_ENV_NAME_FOR_CODEGURU": "handler_module.handler_function"}
63-
64-
def test_it_returns_the_handler_function(self):
65-
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
66-
assert load_handler(self.bootstrap, self.env) == handler_function
67-
68-
def test_it_resets_handler_env_variable(self):
69-
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
70-
load_handler(self.bootstrap, self.env)
71-
assert self.env['_HANDLER'] == "handler_module.handler_function"
72-
73-
class TestWhenHandlerEnvIsMissing:
74-
@before
75-
def before(self):
76-
self.bootstrap = BootstrapModuleMock()
77-
self.env = {}
78-
79-
def test_it_throws_value_error(self):
80-
with pytest.raises(ValueError):
81-
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
82-
load_handler(self.bootstrap, self.env)
65+
class TestGetHandlerExtractor:
66+
class TestWhenRicIsAvailable:
67+
@pytest.fixture(autouse=True)
68+
def around(self):
69+
# simulate that we are in a lambda environment where the awslambdaric.bootstrap module is available
70+
self.module_available = RicBootstrapModuleMock()
71+
sys.modules['awslambdaric.bootstrap'] = self.module_available
72+
yield
73+
del sys.modules['awslambdaric.bootstrap']
74+
75+
def test_it_loads_the_ric_module_code(self):
76+
from codeguru_profiler_agent.aws_lambda.lambda_handler import get_lambda_handler_extractor
77+
result = get_lambda_handler_extractor()
78+
assert result == self.module_available._get_handler
79+
80+
class TestWhenLambdaBootstrapIsAvailable:
81+
@pytest.fixture(autouse=True)
82+
def around(self):
83+
# simulate that we are in a lambda environment where the awslambdaric.bootstrap module is not available
84+
# but bootstrap from lambda is available.
85+
self.module_available = BootstrapModuleMock()
86+
if 'awslambdaric.bootstrap' in sys.modules:
87+
del sys.modules['awslambdaric.bootstrap']
88+
sys.modules['bootstrap'] = self.module_available
89+
yield
90+
del sys.modules['bootstrap']
91+
92+
def test_it_loads_the_lambda_module_code(self):
93+
from codeguru_profiler_agent.aws_lambda.lambda_handler import get_lambda_handler_extractor
94+
result = get_lambda_handler_extractor()
95+
assert result == self.module_available._get_handler
8396

8497
class TestWhenPython36LambdaBootstrapCalls:
8598
class TestWhenHandlerEnvIsSetProperly:
86-
@before
87-
def before(self):
88-
self.bootstrap = BootstrapPython36ModuleMock()
89-
self.env = {"HANDLER_ENV_NAME_FOR_CODEGURU": "handler_module.handler_function"}
99+
@pytest.fixture(autouse=True)
100+
def around(self):
101+
# simulate that we are in a lambda environment where the awslambdaric.bootstrap module is available
102+
sys.modules['bootstrap'] = BootstrapPython36ModuleMock()
90103
global init_handler_has_been_called
91104
init_handler_has_been_called = False
105+
global python36_extractor_has_been_called
106+
python36_extractor_has_been_called = False
107+
yield
108+
del sys.modules['bootstrap']
92109

93-
def test_it_returns_the_handler_function(self):
94-
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
95-
assert load_handler(self.bootstrap, self.env) == handler_function
110+
def test_it_uses_the_old_bootstrap_code(self):
111+
from codeguru_profiler_agent.aws_lambda.lambda_handler import get_lambda_handler_extractor
112+
# call extractor
113+
get_lambda_handler_extractor()("handler_module.handler_function")
114+
assert python36_extractor_has_been_called
96115

97116
def test_it_calls_the_init_handler(self):
98-
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
99-
load_handler(self.bootstrap, self.env)
117+
from codeguru_profiler_agent.aws_lambda.lambda_handler import get_lambda_handler_extractor
118+
# call extractor
119+
get_lambda_handler_extractor()("handler_module.handler_function")
100120
assert init_handler_has_been_called
121+
122+
class TestLoadHandlerFunction:
123+
class TestWhenHandlerEnvIsSetProperly:
124+
@before
125+
def before(self):
126+
self.extractor = BootstrapModuleMock()._get_handler
127+
self.env = {"HANDLER_ENV_NAME_FOR_CODEGURU": "handler_module.handler_function"}
128+
129+
def test_it_returns_the_handler_function(self):
130+
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
131+
assert load_handler(self.extractor, self.env) == handler_function
132+
133+
def test_it_resets_handler_env_variable(self):
134+
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
135+
load_handler(self.extractor, self.env)
136+
assert self.env['_HANDLER'] == "handler_module.handler_function"
137+
138+
class TestWhenHandlerEnvIsMissing:
139+
@before
140+
def before(self):
141+
self.extractor = BootstrapModuleMock()._get_handler
142+
self.env = {}
143+
144+
def test_it_throws_value_error(self):
145+
with pytest.raises(ValueError):
146+
from codeguru_profiler_agent.aws_lambda.lambda_handler import load_handler
147+
load_handler(self.extractor, self.env)

0 commit comments

Comments
 (0)