Skip to content

Commit 53d3a9e

Browse files
committed
SERVER-41295 Add timeouts to burn_in generated tasks
1 parent 7944767 commit 53d3a9e

9 files changed

+533
-185
lines changed

buildscripts/burn_in_tags.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
from shrub.variant import TaskSpec
1212
from shrub.variant import Variant
1313

14+
from evergreen.api import RetryingEvergreenApi
15+
1416
# Get relative imports to work when the package is not installed on the PYTHONPATH.
1517
if __name__ == "__main__" and __package__ is None:
1618
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
1719

1820
# pylint: disable=wrong-import-position
1921
import buildscripts.util.read_config as read_config
2022
from buildscripts.ciconfig import evergreen
21-
from buildscripts.burn_in_tests import create_tests_by_task
22-
from buildscripts.burn_in_tests import create_generate_tasks_config
23+
from buildscripts.burn_in_tests import create_generate_tasks_config, create_tests_by_task
2324
# pylint: enable=wrong-import-position
2425

2526
CONFIG_DIRECTORY = "generated_burn_in_tags_config"
2627
CONFIG_FILE = "burn_in_tags_gen.json"
2728
EVERGREEN_FILE = "etc/evergreen.yml"
29+
EVG_CONFIG_FILE = ".evergreen.yml"
2830

2931
ConfigOptions = namedtuple("ConfigOptions", [
3032
"buildvariant",
@@ -37,6 +39,7 @@
3739
"repeat_tests_secs",
3840
"repeat_tests_min",
3941
"repeat_tests_max",
42+
"project",
4043
])
4144

4245

@@ -59,10 +62,11 @@ def _get_config_options(expansions_file_data, buildvariant, run_buildvariant):
5962
repeat_tests_min = int(expansions_file_data["repeat_tests_min"])
6063
repeat_tests_max = int(expansions_file_data["repeat_tests_max"])
6164
repeat_tests_secs = float(expansions_file_data["repeat_tests_secs"])
65+
project = expansions_file_data["project"]
6266

6367
return ConfigOptions(buildvariant, run_buildvariant, base_commit, max_revisions, branch,
6468
check_evergreen, distro, repeat_tests_secs, repeat_tests_min,
65-
repeat_tests_max)
69+
repeat_tests_max, project)
6670

6771

6872
def _create_evg_buildvariant_map(expansions_file_data):
@@ -110,10 +114,11 @@ def _generate_evg_buildvariant(shrub_config, buildvariant, run_buildvariant):
110114
new_variant.modules(modules)
111115

112116

113-
def _generate_evg_tasks(shrub_config, expansions_file_data, buildvariant_map):
117+
def _generate_evg_tasks(evergreen_api, shrub_config, expansions_file_data, buildvariant_map):
114118
"""
115119
Generate burn in tests tasks for a given shrub config and group of buildvariants.
116120
121+
:param evergreen_api: Evergreen.py object.
117122
:param shrub_config: Shrub config object that the build variants will be built upon.
118123
:param expansions_file_data: Config data file to use.
119124
:param buildvariant_map: Map of base buildvariants to their generated buildvariant.
@@ -123,7 +128,8 @@ def _generate_evg_tasks(shrub_config, expansions_file_data, buildvariant_map):
123128
tests_by_task = create_tests_by_task(config_options)
124129
if tests_by_task:
125130
_generate_evg_buildvariant(shrub_config, buildvariant, run_buildvariant)
126-
create_generate_tasks_config(shrub_config, config_options, tests_by_task, False)
131+
create_generate_tasks_config(evergreen_api, shrub_config, config_options, tests_by_task,
132+
False)
127133

128134

129135
def _write_to_file(shrub_config):
@@ -139,7 +145,7 @@ def _write_to_file(shrub_config):
139145
file_handle.write(shrub_config.to_json())
140146

141147

142-
def main():
148+
def main(evergreen_api):
143149
"""Execute Main program."""
144150

145151
parser = argparse.ArgumentParser(description=main.__doc__)
@@ -150,9 +156,9 @@ def main():
150156

151157
shrub_config = Configuration()
152158
buildvariant_map = _create_evg_buildvariant_map(expansions_file_data)
153-
_generate_evg_tasks(shrub_config, expansions_file_data, buildvariant_map)
159+
_generate_evg_tasks(evergreen_api, shrub_config, expansions_file_data, buildvariant_map)
154160
_write_to_file(shrub_config)
155161

156162

157163
if __name__ == '__main__':
158-
main()
164+
main(RetryingEvergreenApi.get_api(config_file=EVG_CONFIG_FILE))

buildscripts/burn_in_tests.py

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@
1111
import shlex
1212
import sys
1313
import urllib.parse
14+
import datetime
15+
import logging
16+
17+
from math import ceil
1418

15-
import requests
1619
import yaml
20+
import requests
1721

1822
from shrub.config import Configuration
1923
from shrub.command import CommandDefinition
2024
from shrub.task import TaskDependency
2125
from shrub.variant import DisplayTaskDefinition
2226
from shrub.variant import TaskSpec
27+
from shrub.operations import CmdTimeoutUpdate
28+
29+
from evergreen.api import RetryingEvergreenApi
2330

2431
# Get relative imports to work when the package is not installed on the PYTHONPATH.
2532
if __name__ == "__main__" and __package__ is None:
@@ -30,12 +37,20 @@
3037
from buildscripts import resmokelib
3138
from buildscripts.ciconfig import evergreen
3239
from buildscripts.client import evergreen as evergreen_client
40+
from buildscripts.util import teststats
3341
# pylint: enable=wrong-import-position
3442

43+
LOGGER = logging.getLogger(__name__)
44+
3545
API_REST_PREFIX = "/rest/v1/"
3646
API_SERVER_DEFAULT = "https://evergreen.mongodb.com"
47+
AVG_TEST_RUNTIME_ANALYSIS_DAYS = 14
48+
AVG_TEST_TIME_MULTIPLIER = 3
49+
CONFIG_FILE = "../src/.evergreen.yml"
3750
REPEAT_SUITES = 2
3851
EVERGREEN_FILE = "etc/evergreen.yml"
52+
MIN_AVG_TEST_OVERFLOW_SEC = 60
53+
MIN_AVG_TEST_TIME_SEC = 5 * 60
3954
# The executor_file and suite_files defaults are required to make the suite resolver work
4055
# correctly.
4156
SELECTOR_FILE = "etc/burn_in_tests.yml"
@@ -97,6 +112,9 @@ def parse_command_line():
97112
parser.add_option("--reportFile", dest="report_file", default="report.json",
98113
help="Write a JSON file with test results. Default is '%default'.")
99114

115+
parser.add_option("--project", dest="project", default="mongodb-mongo-master",
116+
help="The project the test history will be requested for.")
117+
100118
parser.add_option("--testListFile", dest="test_list_file", default=None, metavar="TESTLIST",
101119
help="Load a JSON file with tests to run.")
102120

@@ -461,7 +479,101 @@ def _get_run_buildvariant(options):
461479
return options.buildvariant
462480

463481

464-
def create_generate_tasks_config(evg_config, options, tests_by_task, include_gen_task):
482+
def _parse_avg_test_runtime(test, task_avg_test_runtime_stats):
483+
"""
484+
Parse list of teststats to find runtime for particular test.
485+
486+
:param task_avg_test_runtime_stats: Teststat data.
487+
:param test: Test name.
488+
:return: Historical average runtime of the test.
489+
"""
490+
for test_stat in task_avg_test_runtime_stats:
491+
if test_stat.test_name == test:
492+
return test_stat.runtime
493+
return None
494+
495+
496+
def _calculate_timeout(avg_test_runtime):
497+
"""
498+
Calculate timeout_secs for the Evergreen task.
499+
500+
:param avg_test_runtime: How long a test has historically taken to run.
501+
:return: The test runtime times AVG_TEST_TIME_MULTIPLIER, or MIN_AVG_TEST_TIME_SEC (whichever
502+
is higher).
503+
"""
504+
return max(MIN_AVG_TEST_TIME_SEC, ceil(avg_test_runtime * AVG_TEST_TIME_MULTIPLIER))
505+
506+
507+
def _calculate_exec_timeout(options, avg_test_runtime):
508+
"""
509+
Calculate exec_timeout_secs for the Evergreen task.
510+
511+
:param avg_test_runtime: How long a test has historically taken to run.
512+
:return: repeat_tests_secs + an amount of padding time so that the test has time to finish on
513+
its final run.
514+
"""
515+
test_execution_time_over_limit = avg_test_runtime - (
516+
options.repeat_tests_secs % avg_test_runtime)
517+
test_execution_time_over_limit = max(MIN_AVG_TEST_OVERFLOW_SEC, test_execution_time_over_limit)
518+
return ceil(options.repeat_tests_secs +
519+
(test_execution_time_over_limit * AVG_TEST_TIME_MULTIPLIER))
520+
521+
522+
def _generate_timeouts(options, commands, test, task_avg_test_runtime_stats):
523+
"""
524+
Add timeout.update command to list of commands for a burn in execution task.
525+
526+
:param options: Command line options.
527+
:param commands: List of commands for a burn in execution task.
528+
:param test: Test name.
529+
:param task_avg_test_runtime_stats: Teststat data.
530+
"""
531+
if task_avg_test_runtime_stats:
532+
avg_test_runtime = _parse_avg_test_runtime(test, task_avg_test_runtime_stats)
533+
if avg_test_runtime:
534+
cmd_timeout = CmdTimeoutUpdate()
535+
LOGGER.debug("Avg test runtime for test %s is: %s", test, avg_test_runtime)
536+
537+
timeout = _calculate_timeout(avg_test_runtime)
538+
cmd_timeout.timeout(timeout)
539+
540+
exec_timeout = _calculate_exec_timeout(options, avg_test_runtime)
541+
cmd_timeout.exec_timeout(exec_timeout)
542+
543+
commands.append(cmd_timeout.validate().resolve())
544+
545+
546+
def _get_task_runtime_history(evergreen_api, project, task, variant):
547+
"""
548+
Fetch historical average runtime for all tests in a task from Evergreen API.
549+
550+
:param evergreen_api: Evergreen API.
551+
:param project: Project name.
552+
:param task: Task name.
553+
:param variant: Variant name.
554+
:return: Test historical runtimes, parsed into teststat objects.
555+
"""
556+
try:
557+
end_date = datetime.datetime.utcnow().replace(microsecond=0)
558+
start_date = end_date - datetime.timedelta(days=AVG_TEST_RUNTIME_ANALYSIS_DAYS)
559+
data = evergreen_api.test_stats_by_project(
560+
project, after_date=start_date.strftime("%Y-%m-%d"),
561+
before_date=end_date.strftime("%Y-%m-%d"), tasks=[task], variants=[variant],
562+
group_by="test", group_num_days=AVG_TEST_RUNTIME_ANALYSIS_DAYS)
563+
test_runtimes = teststats.TestStats(data).get_tests_runtimes()
564+
LOGGER.debug("Test_runtime data parsed from Evergreen history: %s", test_runtimes)
565+
return test_runtimes
566+
except requests.HTTPError as err:
567+
if err.response.status_code == requests.codes.SERVICE_UNAVAILABLE:
568+
# Evergreen may return a 503 when the service is degraded.
569+
# We fall back to returning no test history
570+
return []
571+
else:
572+
raise
573+
574+
575+
def create_generate_tasks_config(evergreen_api, evg_config, options, tests_by_task,
576+
include_gen_task):
465577
"""Create the config for the Evergreen generate.tasks file."""
466578
# pylint: disable=too-many-locals
467579
task_specs = []
@@ -470,6 +582,8 @@ def create_generate_tasks_config(evg_config, options, tests_by_task, include_gen
470582
task_names.append(BURN_IN_TESTS_GEN_TASK)
471583
for task in sorted(tests_by_task):
472584
multiversion_path = tests_by_task[task].get("use_multiversion")
585+
task_avg_test_runtime_stats = _get_task_runtime_history(evergreen_api, options.project,
586+
task, options.buildvariant)
473587
for test_num, test in enumerate(tests_by_task[task]["tests"]):
474588
sub_task_name = _sub_task_name(options, task, test_num)
475589
task_names.append(sub_task_name)
@@ -485,6 +599,7 @@ def create_generate_tasks_config(evg_config, options, tests_by_task, include_gen
485599
get_resmoke_repeat_options(options), test),
486600
}
487601
commands = []
602+
_generate_timeouts(options, commands, test, task_avg_test_runtime_stats)
488603
commands.append(CommandDefinition().function("do setup"))
489604
if multiversion_path:
490605
run_tests_vars["task_path_suffix"] = multiversion_path
@@ -525,11 +640,11 @@ def create_tests_by_task(options):
525640
return tests_by_task
526641

527642

528-
def create_generate_tasks_file(options, tests_by_task):
643+
def create_generate_tasks_file(evergreen_api, options, tests_by_task):
529644
"""Create the Evergreen generate.tasks file."""
530645

531646
evg_config = Configuration()
532-
evg_config = create_generate_tasks_config(evg_config, options, tests_by_task,
647+
evg_config = create_generate_tasks_config(evergreen_api, evg_config, options, tests_by_task,
533648
include_gen_task=True)
534649
_write_json_file(evg_config.to_map(), options.generate_tasks_file)
535650

@@ -561,9 +676,15 @@ def run_tests(no_exec, tests_by_task, resmoke_cmd, report_file):
561676
_write_json_file(test_results, report_file)
562677

563678

564-
def main():
679+
def main(evergreen_api):
565680
"""Execute Main program."""
566681

682+
logging.basicConfig(
683+
format="[%(asctime)s - %(name)s - %(levelname)s] %(message)s",
684+
level=logging.DEBUG,
685+
stream=sys.stdout,
686+
)
687+
567688
options, args = parse_command_line()
568689

569690
resmoke_cmd = _set_resmoke_cmd(options, args)
@@ -585,10 +706,10 @@ def main():
585706
_write_json_file(tests_by_task, options.test_list_outfile)
586707

587708
if options.generate_tasks_file:
588-
create_generate_tasks_file(options, tests_by_task)
709+
create_generate_tasks_file(evergreen_api, options, tests_by_task)
589710
else:
590711
run_tests(options.no_exec, tests_by_task, resmoke_cmd, options.report_file)
591712

592713

593714
if __name__ == "__main__":
594-
main()
715+
main(RetryingEvergreenApi.get_api(config_file=CONFIG_FILE))

0 commit comments

Comments
 (0)