Skip to content

Commit d36e1d7

Browse files
committed
fix(aws-lambda): Load handler module only once in the layer (#8079)
Cache the resolved AWSLambdaModuleLoader and entry point handler at module scope in init_serverless_sdk so subsequent Lambda invocations reuse the cached handler without re-importing or re-executing the module on each invocation. Closes #8079
1 parent 9e4ba4c commit d36e1d7

2 files changed

Lines changed: 139 additions & 3 deletions

File tree

‎scripts/init_serverless_sdk.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,23 @@ def get_lambda_handler(self):
6969
return getattr(self.lambda_function_module, self.handler_name)
7070

7171

72-
def sentry_lambda_handler(event: "Any", context: "Any") -> None:
72+
_entry_point: "Any" = None
73+
_module_loader: "AWSLambdaModuleLoader | None" = None
74+
75+
76+
def _reset_loader() -> None:
77+
global _entry_point, _module_loader
78+
_entry_point = None
79+
_module_loader = None
80+
81+
82+
def sentry_lambda_handler(event: "Any", context: "Any") -> "Any":
7383
"""
7484
Handler function that invokes a lambda handler which path is defined in
7585
environment variables as "SENTRY_INITIAL_HANDLER"
7686
"""
77-
module_loader = AWSLambdaModuleLoader(os.environ["SENTRY_INITIAL_HANDLER"])
78-
return module_loader.get_lambda_handler()(event, context)
87+
global _entry_point, _module_loader
88+
if _entry_point is None:
89+
_module_loader = AWSLambdaModuleLoader(os.environ["SENTRY_INITIAL_HANDLER"])
90+
_entry_point = _module_loader.get_lambda_handler()
91+
return _entry_point(event, context)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from unittest import mock
2+
3+
import pytest
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def init_serverless_sdk_module(monkeypatch):
8+
monkeypatch.setenv("SENTRY_DSN", "https://foo@sentry.io/42")
9+
monkeypatch.setenv("SENTRY_TRACES_SAMPLE_RATE", "1.0")
10+
import scripts.init_serverless_sdk as init_sdk
11+
12+
init_sdk._reset_loader()
13+
yield init_sdk
14+
init_sdk._reset_loader()
15+
16+
17+
def test_handler_loaded_only_once(init_serverless_sdk_module, monkeypatch):
18+
init_sdk = init_serverless_sdk_module
19+
20+
mock_handler = mock.MagicMock(return_value={"status": "ok"})
21+
22+
monkeypatch.setenv("SENTRY_INITIAL_HANDLER", "fake_module.my_handler")
23+
24+
with mock.patch(
25+
"scripts.init_serverless_sdk.AWSLambdaModuleLoader"
26+
) as mock_loader_cls:
27+
instance = mock_loader_cls.return_value
28+
instance.get_lambda_handler.return_value = mock_handler
29+
30+
# First invocation
31+
res1 = init_sdk.sentry_lambda_handler({"key": "val1"}, {"context": 1})
32+
assert res1 == {"status": "ok"}
33+
assert mock_loader_cls.call_count == 1
34+
assert instance.get_lambda_handler.call_count == 1
35+
assert mock_handler.call_count == 1
36+
mock_handler.assert_called_with({"key": "val1"}, {"context": 1})
37+
38+
# Second invocation - should reuse cached handler without creating new AWSLambdaModuleLoader
39+
res2 = init_sdk.sentry_lambda_handler({"key": "val2"}, {"context": 2})
40+
assert res2 == {"status": "ok"}
41+
assert mock_loader_cls.call_count == 1
42+
assert instance.get_lambda_handler.call_count == 1
43+
assert mock_handler.call_count == 2
44+
mock_handler.assert_called_with({"key": "val2"}, {"context": 2})
45+
46+
# Third invocation
47+
res3 = init_sdk.sentry_lambda_handler({"key": "val3"}, {"context": 3})
48+
assert res3 == {"status": "ok"}
49+
assert mock_loader_cls.call_count == 1
50+
assert mock_handler.call_count == 3
51+
52+
53+
def test_handler_execution_with_real_loader(
54+
init_serverless_sdk_module, monkeypatch, tmp_path
55+
):
56+
init_sdk = init_serverless_sdk_module
57+
58+
# Create a real python file with a handler
59+
test_file = tmp_path / "my_lambda.py"
60+
test_file.write_text(
61+
"invocations = 0\n"
62+
"def handler(event, context):\n"
63+
" global invocations\n"
64+
" invocations += 1\n"
65+
" return {'count': invocations, 'event': event}\n"
66+
)
67+
68+
monkeypatch.syspath_prepend(str(tmp_path))
69+
monkeypatch.setenv("SENTRY_INITIAL_HANDLER", "my_lambda.handler")
70+
71+
original_extract = (
72+
init_sdk.AWSLambdaModuleLoader.extract_and_load_lambda_function_module
73+
)
74+
with mock.patch.object(
75+
init_sdk.AWSLambdaModuleLoader,
76+
"extract_and_load_lambda_function_module",
77+
side_effect=original_extract,
78+
autospec=True,
79+
) as mock_extract:
80+
# First call
81+
res1 = init_sdk.sentry_lambda_handler({"call": 1}, None)
82+
assert res1["count"] == 1
83+
assert res1["event"] == {"call": 1}
84+
assert mock_extract.call_count == 1
85+
86+
# Second call: handler module should NOT be re-extracted / reloaded
87+
res2 = init_sdk.sentry_lambda_handler({"call": 2}, None)
88+
assert res2["count"] == 2
89+
assert res2["event"] == {"call": 2}
90+
assert mock_extract.call_count == 1
91+
92+
93+
def test_reset_loader_allows_reloading(init_serverless_sdk_module, monkeypatch):
94+
init_sdk = init_serverless_sdk_module
95+
96+
mock_handler = mock.MagicMock(return_value="ok")
97+
monkeypatch.setenv("SENTRY_INITIAL_HANDLER", "fake_module.my_handler")
98+
99+
with mock.patch(
100+
"scripts.init_serverless_sdk.AWSLambdaModuleLoader"
101+
) as mock_loader_cls:
102+
mock_loader_cls.return_value.get_lambda_handler.return_value = mock_handler
103+
104+
init_sdk.sentry_lambda_handler({}, {})
105+
assert mock_loader_cls.call_count == 1
106+
107+
# Reset loader
108+
init_sdk._reset_loader()
109+
110+
init_sdk.sentry_lambda_handler({}, {})
111+
assert mock_loader_cls.call_count == 2
112+
113+
114+
def test_invalid_handler_path_raises_error(init_serverless_sdk_module, monkeypatch):
115+
init_sdk = init_serverless_sdk_module
116+
117+
monkeypatch.setenv("SENTRY_INITIAL_HANDLER", "invalid_handler_no_dot")
118+
119+
with pytest.raises(ValueError, match="Incorrect AWS Handler path"):
120+
init_sdk.sentry_lambda_handler({}, {})
121+
122+
assert init_sdk._entry_point is None
123+
assert init_sdk._module_loader is None

0 commit comments

Comments
 (0)