Skip to content

Commit 4da26bb

Browse files
authored
Merge pull request #44 from aws/python39_bootstrap
Update lambda handler function extraction to work with python 3.9
2 parents 75fc2a2 + e248ac7 commit 4da26bb

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)