Skip to content

Commit f107b98

Browse files
author
Yann
authored
Merge pull request #328 from DataDog/quentin.pierre/aws-lambda-decorator
Lambda wrapper - start TLS connection on init & RequestClientKeptAlive
2 parents 90c0756 + ab02154 commit f107b98

File tree

4 files changed

+75
-27
lines changed

4 files changed

+75
-27
lines changed

datadog/api/http_client.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# stdlib
99
import logging
1010
import urllib
11+
from threading import Lock
1112

1213
# 3p
1314
try:
@@ -59,26 +60,30 @@ def request(cls, method, url, headers, params, data, timeout, proxies, verify, m
5960

6061
class RequestClient(HTTPClient):
6162
"""
62-
HTTP client based on 3rd party `requests` module.
63+
HTTP client based on 3rd party `requests` module, using a single session.
64+
This allows us to keep the session alive to spare some execution time.
6365
"""
66+
67+
_session = None
68+
_session_lock = Lock()
69+
6470
@classmethod
6571
def request(cls, method, url, headers, params, data, timeout, proxies, verify, max_retries):
66-
"""
67-
"""
6872
try:
69-
# Use a session to set a max_retries parameters
70-
with requests.Session() as s:
71-
http_adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
72-
s.mount('https://', http_adapter)
73-
74-
# Since stream=False we can close the session after this call
75-
result = s.request(
76-
method, url,
77-
headers=headers, params=params, data=data,
78-
timeout=timeout,
79-
proxies=proxies, verify=verify)
80-
81-
result.raise_for_status()
73+
74+
with cls._session_lock:
75+
if cls._session is None:
76+
cls._session = requests.Session()
77+
http_adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
78+
cls._session.mount('https://', http_adapter)
79+
80+
result = cls._session.request(
81+
method, url,
82+
headers=headers, params=params, data=data,
83+
timeout=timeout,
84+
proxies=proxies, verify=verify)
85+
86+
result.raise_for_status()
8287

8388
except requests.ConnectionError as e:
8489
raise _remove_context(ClientError(method, url, e))

datadog/threadstats/aws_lambda.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datadog.threadstats import ThreadStats
2-
from threading import Lock
2+
from threading import Lock, Thread
33
from datadog import api
44
import os
55

@@ -34,6 +34,11 @@ def _enter(cls):
3434
cls._was_initialized = True
3535
api._api_key = os.environ.get('DATADOG_API_KEY')
3636
api._api_host = os.environ.get('DATADOG_HOST', 'https://api.datadoghq.com')
37+
38+
# Async initialization of the TLS connection with our endpoints
39+
# This avoids adding execution time at the end of the lambda run
40+
t = Thread(target=_init_api_client)
41+
t.start()
3742
cls._counter = cls._counter + 1
3843

3944
@classmethod
@@ -70,3 +75,19 @@ def __call__(self, *args, **kw):
7075
def lambda_metric(*args, **kw):
7176
""" Alias to expose only distributions for lambda functions"""
7277
_lambda_stats.distribution(*args, **kw)
78+
79+
80+
def _init_api_client():
81+
""" No-op GET to initialize the requests connection with DD's endpoints
82+
83+
The goal here is to make the final flush faster:
84+
we keep alive the Requests session, this means that we can re-use the connection
85+
The consequence is that the HTTP Handshake, which can take hundreds of ms,
86+
is now made at the beginning of a lambda instead of at the end.
87+
88+
By making the initial request async, we spare a lot of execution time in the lambdas.
89+
"""
90+
try:
91+
api.api_client.APIClient.submit('GET', 'validate')
92+
except Exception:
93+
pass

tests/unit/api/helper.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
# datadog
1010
from datadog import initialize, api
11+
from datadog.api.http_client import RequestClient
1112
from datadog.api.exceptions import ApiError
1213
from datadog.api.resources import (
1314
CreateableAPIResource,
@@ -33,6 +34,25 @@
3334
}
3435

3536

37+
class MockSession(object):
38+
"""docstring for MockSession"""
39+
_args = None
40+
_kwargs = None
41+
_count = 0
42+
43+
def request(self, *args, **kwargs):
44+
self._args = args
45+
self._kwargs = kwargs
46+
self._count += 1
47+
return MockResponse()
48+
49+
def call_args(self):
50+
return self._args, self._kwargs
51+
52+
def call_count(self):
53+
return self._count
54+
55+
3656
class MockResponse(requests.Response):
3757

3858
def __init__(self, raise_for_status=False):
@@ -98,13 +118,15 @@ class DatadogAPITestCase(unittest.TestCase):
98118

99119
def setUp(self):
100120
# Mock patch requests
101-
self.request_patcher = patch('requests.Session')
102-
request_class_mock = self.request_patcher.start()
103-
self.request_mock = request_class_mock.return_value.__enter__.return_value
104-
self.request_mock.request = Mock(return_value=MockResponse())
121+
self.request_mock = MockSession()
122+
RequestClient._session = self.request_mock
123+
# self.request_patcher = patch('requests.Session')
124+
# request_class_mock = self.request_patcher.start()
125+
# self.request_mock = request_class_mock.return_value
126+
# self.request_mock.request = Mock(return_value=MockResponse())
105127

106128
def tearDown(self):
107-
self.request_patcher.stop()
129+
del RequestClient._session
108130

109131
def arm_requests_to_raise(self):
110132
"""
@@ -116,11 +138,11 @@ def get_request_data(self):
116138
"""
117139
Returns JSON formatted data from the submitted `requests`.
118140
"""
119-
_, kwargs = self.request_mock.request.call_args
141+
_, kwargs = self.request_mock.call_args()
120142
return json.loads(kwargs['data'])
121143

122144
def request_called_with(self, method, url, data=None, params=None):
123-
(req_method, req_url), others = self.request_mock.request.call_args
145+
(req_method, req_url), others = self.request_mock.call_args()
124146
self.assertEquals(method, req_method, req_method)
125147
self.assertEquals(url, req_url, req_url)
126148

tests/unit/api/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_no_initialization_fails(self):
7373
# Finally, initialize with an API key
7474
initialize(api_key=API_KEY, api_host=API_HOST)
7575
MyCreatable.create()
76-
self.assertEquals(self.request_mock.request.call_count, 1)
76+
self.assertEquals(self.request_mock.call_count(), 1)
7777

7878
@mock.patch('datadog.util.config.get_config_path')
7979
def test_get_hostname(self, mock_config_path):
@@ -105,7 +105,7 @@ def test_request_parameters(self):
105105
# Make a simple API call
106106
MyCreatable.create()
107107

108-
_, options = self.request_mock.request.call_args
108+
_, options = self.request_mock.call_args()
109109

110110
# Assert `requests` parameters
111111
self.assertIn('params', options)
@@ -128,7 +128,7 @@ def test_initialize_options(self):
128128
# Make a simple API call
129129
MyCreatable.create()
130130

131-
_, options = self.request_mock.request.call_args
131+
_, options = self.request_mock.call_args()
132132

133133
# Assert `requests` parameters
134134
self.assertIn('proxies', options)

0 commit comments

Comments
 (0)