Commit cc2fe9b7 authored by Scott Lee's avatar Scott Lee Committed by LUCI CQ

[resultdb] use requests.Session in rdb_wrapper

requests.post() creates a new HTTP connection for each call.
It makes presubmit checks slower and timed out.
This CL updates rdb_wrapper to use requests.Session() to use
a connection pool for Sink requests.

Change-Id: Id02ecce85898ae15993d53df17473bb5dfb7e89f
Bug: 1145762
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2532895
Commit-Queue: Scott Lee <ddoman@chromium.org>
Reviewed-by: 's avatarChan Li <chanli@chromium.org>
Reviewed-by: 's avatarErik Staab <estaab@chromium.org>
Reviewed-by: 's avatarDirk Pranke <dpranke@google.com>
parent 55896522
...@@ -1581,30 +1581,34 @@ class PresubmitExecuter(object): ...@@ -1581,30 +1581,34 @@ class PresubmitExecuter(object):
results = [] results = []
try: try:
if 'PRESUBMIT_VERSION' in context and \ version = [
[int(x) for x in context['PRESUBMIT_VERSION'].split('.')] >= [2, 0, 0]: int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
for function_name in context: ]
if not function_name.startswith('Check'):
continue with rdb_wrapper.client(prefix) as sink:
if function_name.endswith('Commit') and not self.committing: if version >= [2, 0, 0]:
continue for function_name in context:
if function_name.endswith('Upload') and self.committing: if not function_name.startswith('Check'):
continue continue
logging.debug('Running %s in %s', function_name, presubmit_path) if function_name.endswith('Commit') and not self.committing:
results.extend( continue
self._run_check_function(function_name, context, prefix)) if function_name.endswith('Upload') and self.committing:
logging.debug('Running %s done.', function_name) continue
self.more_cc.extend(output_api.more_cc) logging.debug('Running %s in %s', function_name, presubmit_path)
results.extend(
else: # Old format self._run_check_function(function_name, context, sink))
if self.committing: logging.debug('Running %s done.', function_name)
function_name = 'CheckChangeOnCommit' self.more_cc.extend(output_api.more_cc)
else:
function_name = 'CheckChangeOnUpload' else: # Old format
if function_name in context: if self.committing:
function_name = 'CheckChangeOnCommit'
else:
function_name = 'CheckChangeOnUpload'
if function_name in context:
logging.debug('Running %s in %s', function_name, presubmit_path) logging.debug('Running %s in %s', function_name, presubmit_path)
results.extend( results.extend(
self._run_check_function(function_name, context, prefix)) self._run_check_function(function_name, context, sink))
logging.debug('Running %s done.', function_name) logging.debug('Running %s done.', function_name)
self.more_cc.extend(output_api.more_cc) self.more_cc.extend(output_api.more_cc)
...@@ -1616,23 +1620,37 @@ class PresubmitExecuter(object): ...@@ -1616,23 +1620,37 @@ class PresubmitExecuter(object):
os.chdir(main_path) os.chdir(main_path)
return results return results
def _run_check_function(self, function_name, context, prefix): def _run_check_function(self, function_name, context, sink=None):
"""Evaluates a presubmit check function, function_name, in the context """Evaluates and returns the result of a given presubmit function.
provided. If LUCI_CONTEXT is enabled, it will send the result to ResultSink.
Passes function_name and prefix to rdb_wrapper.setup_rdb. Returns results. If sink is given, the result of the presubmit function will be reported
to the ResultSink.
Args: Args:
function_name: a string representing the name of the function to run function_name: the name of the presubmit function to evaluate
context: a context dictionary in which the function will be evaluated context: a context dictionary in which the function will be evaluated
prefix: a string describing prefix for ResultDB test id sink: an instance of ResultSink. None, by default.
Returns:
Returns: Results from evaluating the function call.""" the result of the presubmit function call.
with rdb_wrapper.setup_rdb(function_name, prefix) as my_status: """
start_time = time_time()
try:
result = eval(function_name + '(*__args)', context) result = eval(function_name + '(*__args)', context)
self._check_result_type(result) self._check_result_type(result)
if any(res.fatal for res in result): except:
my_status.status = rdb_wrapper.STATUS_FAIL if sink:
return result elapsed_time = time_time() - start_time
sink.report(function_name, rdb_wrapper.STATUS_FAIL, elapsed_time)
raise
if sink:
elapsed_time = time_time() - start_time
status = rdb_wrapper.STATUS_PASS
if any(r.fatal for r in result):
status = rdb_wrapper.STATUS_FAIL
sink.report(function_name, status, elapsed_time)
return result
def _check_result_type(self, result): def _check_result_type(self, result):
"""Helper function which ensures result is a list, and all elements are """Helper function which ensures result is a list, and all elements are
......
...@@ -16,52 +16,65 @@ STATUS_CRASH = 'CRASH' ...@@ -16,52 +16,65 @@ STATUS_CRASH = 'CRASH'
STATUS_ABORT = 'ABORT' STATUS_ABORT = 'ABORT'
STATUS_SKIP = 'SKIP' STATUS_SKIP = 'SKIP'
class ResultSinkStatus(object):
def __init__(self): class ResultSink(object):
self.status = STATUS_PASS def __init__(self, session, url, prefix):
self._session = session
self._url = url
self._prefix = prefix
def report(self, function_name, status, elapsed_time):
"""Reports the result and elapsed time of a presubmit function call.
Args:
function_name (str): The name of the presubmit function
status: the status to report the function call with
elapsed_time: the time taken to invoke the presubmit function
"""
tr = {
'testId': self._prefix + function_name,
'status': status,
'expected': status == STATUS_PASS,
'duration': '{:.9f}s'.format(elapsed_time)
}
self._session.post(self._url, json={'testResults': [tr]})
@contextlib.contextmanager @contextlib.contextmanager
def setup_rdb(function_name, prefix): def client(prefix):
"""Context Manager function for ResultDB reporting. """Returns a client for ResultSink.
This is a context manager that returns a client for ResultSink,
if LUCI_CONTEXT with a section of result_sink is present. When the context
is closed, all the connetions to the SinkServer are closed.
Args: Args:
function_name (str): The name of the function we are about to run. prefix: A prefix to be added to the test ID of reported function names.
prefix (str): The prefix for the name of the test. The format for this is The format for this is
presubmit:gerrit_host/folder/to/repo:path/to/file/ presubmit:gerrit_host/folder/to/repo:path/to/file/
for example, for example,
presubmit:chromium-review.googlesource.com/chromium/src/:services/viz/ presubmit:chromium-review.googlesource.com/chromium/src/:services/viz/
Returns:
An instance of ResultSink() if the luci context is present. None, otherwise.
""" """
sink = None luci_ctx = os.environ.get('LUCI_CONTEXT')
if 'LUCI_CONTEXT' in os.environ: if not luci_ctx:
with open(os.environ['LUCI_CONTEXT']) as f: yield None
j = json.load(f) return
if 'result_sink' in j:
sink = j['result_sink'] sink_ctx = None
with open(luci_ctx) as f:
sink_ctx = json.load(f).get('result_sink')
if not sink_ctx:
yield None
return
my_status = ResultSinkStatus() url = 'http://{0}/prpc/luci.resultsink.v1.Sink/ReportTestResults'.format(
start_time = time.time() sink_ctx['address'])
try: with requests.Session() as s:
yield my_status s.headers = {
except Exception: 'Content-Type': 'application/json',
my_status.status = STATUS_FAIL 'Accept': 'application/json',
raise 'Authorization': 'ResultSink {0}'.format(sink_ctx['auth_token'])
finally: }
end_time = time.time() yield ResultSink(s, url, prefix)
elapsed_time = end_time - start_time
if sink != None:
tr = {
'testId': '{0}:{1}'.format(prefix, function_name),
'status': my_status.status,
'expected': (my_status.status == STATUS_PASS),
'duration': '{:.9f}s'.format(elapsed_time)
}
requests.post(
url='http://{0}/prpc/luci.resultsink.v1.Sink/ReportTestResults'
.format(sink['address']),
headers={
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'ResultSink {0}'.format(sink['auth_token'])
},
data=json.dumps({'testResults': [tr]})
)
...@@ -46,6 +46,7 @@ import json ...@@ -46,6 +46,7 @@ import json
import owners import owners
import owners_finder import owners_finder
import presubmit_support as presubmit import presubmit_support as presubmit
import rdb_wrapper
import scm import scm
import subprocess2 as subprocess import subprocess2 as subprocess
...@@ -168,6 +169,7 @@ index fe3de7b..54ae6e1 100755 ...@@ -168,6 +169,7 @@ index fe3de7b..54ae6e1 100755
presubmit._ASKED_FOR_FEEDBACK = False presubmit._ASKED_FOR_FEEDBACK = False
self.fake_root_dir = self.RootDir() self.fake_root_dir = self.RootDir()
self.fake_change = FakeChange(self) self.fake_change = FakeChange(self)
self.rdb_client = mock.MagicMock()
mock.patch('gclient_utils.FileRead').start() mock.patch('gclient_utils.FileRead').start()
mock.patch('gclient_utils.FileWrite').start() mock.patch('gclient_utils.FileWrite').start()
...@@ -180,7 +182,8 @@ index fe3de7b..54ae6e1 100755 ...@@ -180,7 +182,8 @@ index fe3de7b..54ae6e1 100755
mock.patch('os.path.isfile').start() mock.patch('os.path.isfile').start()
mock.patch('os.remove').start() mock.patch('os.remove').start()
mock.patch('presubmit_support._parse_files').start() mock.patch('presubmit_support._parse_files').start()
mock.patch('presubmit_support.rdb_wrapper.setup_rdb').start() mock.patch('presubmit_support.rdb_wrapper.client',
return_value=self.rdb_client).start()
mock.patch('presubmit_support.sigint_handler').start() mock.patch('presubmit_support.sigint_handler').start()
mock.patch('presubmit_support.time_time', return_value=0).start() mock.patch('presubmit_support.time_time', return_value=0).start()
mock.patch('presubmit_support.warn').start() mock.patch('presubmit_support.warn').start()
...@@ -538,14 +541,45 @@ class PresubmitUnittest(PresubmitTestsBase): ...@@ -538,14 +541,45 @@ class PresubmitUnittest(PresubmitTestsBase):
' return results\n', ' return results\n',
fake_presubmit)) fake_presubmit))
presubmit.rdb_wrapper.setup_rdb.assert_called()
self.assertRaises(presubmit.PresubmitFailure, self.assertRaises(presubmit.PresubmitFailure,
executer.ExecPresubmitScript, executer.ExecPresubmitScript,
'def CheckChangeOnCommit(input_api, output_api):\n' 'def CheckChangeOnCommit(input_api, output_api):\n'
' return ["foo"]', ' return ["foo"]',
fake_presubmit) fake_presubmit)
def testExecPresubmitScriptWithResultDB(self):
description_lines = ('Hello there', 'this is a change', 'BUG=123')
files = [['A', 'foo\\blat.cc']]
fake_presubmit = os.path.join(self.fake_root_dir, 'PRESUBMIT.py')
change = presubmit.Change('mychange', '\n'.join(description_lines),
self.fake_root_dir, files, 0, 0, None)
executer = presubmit.PresubmitExecuter(change, True, None, False)
sink = self.rdb_client.__enter__.return_value = mock.MagicMock()
# STATUS_PASS on success
executer.ExecPresubmitScript(
'def CheckChangeOnCommit(input_api, output_api):\n'
' return [output_api.PresubmitResult("test")]\n', fake_presubmit)
sink.report.assert_called_with('CheckChangeOnCommit',
rdb_wrapper.STATUS_PASS, 0)
# STATUS_FAIL on exception
sink.reset_mock()
self.assertRaises(
Exception, executer.ExecPresubmitScript,
'def CheckChangeOnCommit(input_api, output_api):\n'
' raise Exception("boom")', fake_presubmit)
sink.report.assert_called_with('CheckChangeOnCommit',
rdb_wrapper.STATUS_FAIL, 0)
# STATUS_FAIL on fatal error
sink.reset_mock()
executer.ExecPresubmitScript(
'def CheckChangeOnCommit(input_api, output_api):\n'
' return [output_api.PresubmitError("error")]\n', fake_presubmit)
sink.report.assert_called_with('CheckChangeOnCommit',
rdb_wrapper.STATUS_FAIL, 0)
def testExecPresubmitScriptTemporaryFilesRemoval(self): def testExecPresubmitScriptTemporaryFilesRemoval(self):
tempfile.NamedTemporaryFile.side_effect = [ tempfile.NamedTemporaryFile.side_effect = [
MockTemporaryFile('baz'), MockTemporaryFile('baz'),
......
...@@ -7,80 +7,96 @@ ...@@ -7,80 +7,96 @@
from __future__ import print_function from __future__ import print_function
import contextlib
import json import json
import logging import logging
import os import os
import requests import requests
import sys import sys
import tempfile
import time import time
import unittest import unittest
if sys.version_info.major == 2: if sys.version_info.major == 2:
import mock import mock
BUILTIN_OPEN = '__builtin__.open'
else: else:
from unittest import mock from unittest import mock
BUILTIN_OPEN = 'builtins.open'
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import rdb_wrapper import rdb_wrapper
class TestSetupRDB(unittest.TestCase):
def setUp(self):
super(TestSetupRDB, self).setUp()
mock.patch(BUILTIN_OPEN, mock.mock_open(read_data =
'''{"result_sink":{"address": "fakeAddr","auth_token" : "p@$$w0rD"}}''')
).start()
mock.patch('os.environ', {'LUCI_CONTEXT': 'dummy_file.txt'}).start()
mock.patch('requests.post').start()
mock.patch('time.time', side_effect=[1.0, 2.0, 3.0, 4.0, 5.0]).start()
def test_setup_rdb(self):
with rdb_wrapper.setup_rdb("_foobar", './my/folder/') as my_status_obj:
self.assertEqual(my_status_obj.status, rdb_wrapper.STATUS_PASS)
my_status_obj.status = rdb_wrapper.STATUS_FAIL
expectedTr = {
'testId' : './my/folder/:_foobar',
'status' : rdb_wrapper.STATUS_FAIL,
'expected': False,
'duration': '1.000000000s'
}
requests.post.assert_called_once_with( @contextlib.contextmanager
url='http://fakeAddr/prpc/luci.resultsink.v1.Sink/ReportTestResults', def lucictx(ctx):
headers={ try:
'Content-Type': 'application/json', orig = os.environ.get('LUCI_CONTEXT')
'Accept': 'application/json',
'Authorization': 'ResultSink p@$$w0rD'
},
data=json.dumps({'testResults': [expectedTr]})
)
def test_setup_rdb_exception(self): if ctx is None:
with self.assertRaises(Exception): os.environ.pop('LUCI_CONTEXT', '')
with rdb_wrapper.setup_rdb("_foobar", './my/folder/'): yield
raise Exception("Generic Error") else:
# windows doesn't allow a file to be opened twice at the same time.
# therefore, this closes the temp file before yield, so that
# rdb_wrapper.client() can open the LUCI_CONTEXT file.
f = tempfile.NamedTemporaryFile(delete=False)
f.write(json.dumps(ctx).encode('utf-8'))
f.close()
os.environ['LUCI_CONTEXT'] = f.name
yield
os.unlink(f.name)
expectedTr = { finally:
'testId': './my/folder/:_foobar', if orig is None:
'status': rdb_wrapper.STATUS_FAIL, os.environ.pop('LUCI_CONTEXT', '')
'expected': False, else:
'duration': '1.000000000s' os.environ['LUCI_CONTEXT'] = orig
}
@mock.patch.dict(os.environ, {})
class TestClient(unittest.TestCase):
def test_without_lucictx(self):
with lucictx(None):
with rdb_wrapper.client("prefix") as s:
self.assertIsNone(s)
requests.post.assert_called_once_with( with lucictx({'something else': {'key': 'value'}}):
url='http://fakeAddr/prpc/luci.resultsink.v1.Sink/ReportTestResults', with rdb_wrapper.client("prefix") as s:
headers={ self.assertIsNone(s)
'Content-Type': 'application/json',
'Accept': 'application/json', def test_with_lucictx(self):
'Authorization': 'ResultSink p@$$w0rD' with lucictx({'result_sink': {'address': '127', 'auth_token': 'secret'}}):
}, with rdb_wrapper.client("prefix") as s:
data=json.dumps({'testResults': [expectedTr]}) self.assertIsNotNone(s)
self.assertEqual(
s._url,
'http://127/prpc/luci.resultsink.v1.Sink/ReportTestResults',
)
self.assertDictEqual(
s._session.headers, {
'Accept': 'application/json',
'Authorization': 'ResultSink secret',
'Content-Type': 'application/json',
})
class TestResultSink(unittest.TestCase):
def test_report(self):
session = mock.MagicMock()
sink = rdb_wrapper.ResultSink(session, 'http://host', 'test_id_prefix/')
sink.report("function_foo", rdb_wrapper.STATUS_PASS, 123)
expected = {
'testId': 'test_id_prefix/function_foo',
'status': rdb_wrapper.STATUS_PASS,
'expected': True,
'duration': '123.000000000s',
}
session.post.assert_called_once_with(
'http://host',
json={'testResults': [expected]},
) )
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG if '-v' in sys.argv else logging.ERROR) level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment