summaryrefslogtreecommitdiff
path: root/buildscripts/timeouts/timeout.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildscripts/timeouts/timeout.py')
-rw-r--r--buildscripts/timeouts/timeout.py121
1 files changed, 121 insertions, 0 deletions
diff --git a/buildscripts/timeouts/timeout.py b/buildscripts/timeouts/timeout.py
new file mode 100644
index 00000000000..3e3440f9c5b
--- /dev/null
+++ b/buildscripts/timeouts/timeout.py
@@ -0,0 +1,121 @@
+"""Timeout information for generating tasks."""
+import math
+from datetime import timedelta
+from inspect import currentframe, getframeinfo
+from typing import NamedTuple, Optional
+
+import structlog
+from buildscripts.patch_builds.task_generation import TimeoutInfo
+
+LOGGER = structlog.getLogger(__name__)
+
+AVG_TASK_SETUP_TIME = int(timedelta(minutes=2).total_seconds())
+MIN_TIMEOUT_SECONDS = int(timedelta(minutes=5).total_seconds())
+MAX_EXPECTED_TIMEOUT = int(timedelta(hours=48).total_seconds())
+DEFAULT_SCALING_FACTOR = 3.0
+
+
+def calculate_timeout(avg_runtime: float, scaling_factor: Optional[float] = None) -> int:
+ """
+ Determine how long a runtime to set based on average runtime and a scaling factor.
+
+ :param avg_runtime: Average runtime of previous runs.
+ :param scaling_factor: Scaling factor for timeout.
+ :return: timeout to use (in seconds).
+ """
+
+ scaling_factor = DEFAULT_SCALING_FACTOR if not scaling_factor else scaling_factor
+
+ def round_to_minute(runtime):
+ """Round the given seconds up to the nearest minute."""
+ distance_to_min = 60 - (runtime % 60)
+ return int(math.ceil(runtime + distance_to_min))
+
+ return max(MIN_TIMEOUT_SECONDS, round_to_minute(avg_runtime * scaling_factor))
+
+
+class TimeoutEstimate(NamedTuple):
+ """Runtime estimates used to calculate timeouts."""
+
+ max_test_runtime: Optional[float]
+ expected_task_runtime: Optional[float]
+
+ @classmethod
+ def no_timeouts(cls) -> "TimeoutEstimate":
+ """Create an instance with no estimation data."""
+ return cls(max_test_runtime=None, expected_task_runtime=None)
+
+ def is_specified(self) -> bool:
+ """Determine if any specific timeout value has been specified."""
+ return self.max_test_runtime is not None or self.expected_task_runtime is not None
+
+ def calculate_test_timeout(self, repeat_factor: int,
+ scaling_factor: Optional[float] = None) -> Optional[int]:
+ """
+ Calculate the timeout to use for tests.
+
+ :param repeat_factor: How many times the suite will be repeated.
+ :param scaling_factor: Scaling factor for timeout.
+ :return: Timeout value to use for tests.
+ """
+ if self.max_test_runtime is None:
+ return None
+
+ timeout = calculate_timeout(self.max_test_runtime, scaling_factor) * repeat_factor
+ LOGGER.debug("Setting timeout", timeout=timeout, max_runtime=self.max_test_runtime,
+ repeat_factor=repeat_factor, scaling_factor=(scaling_factor
+ or DEFAULT_SCALING_FACTOR))
+ return timeout
+
+ def calculate_task_timeout(self, repeat_factor: int,
+ scaling_factor: Optional[float] = None) -> Optional[int]:
+ """
+ Calculate the timeout to use for tasks.
+
+ :param repeat_factor: How many times the suite will be repeated.
+ :param scaling_factor: Scaling factor for timeout.
+ :return: Timeout value to use for tasks.
+ """
+ if self.expected_task_runtime is None:
+ return None
+
+ exec_timeout = calculate_timeout(self.expected_task_runtime,
+ scaling_factor) * repeat_factor + AVG_TASK_SETUP_TIME
+ LOGGER.debug("Setting exec_timeout", exec_timeout=exec_timeout,
+ suite_runtime=self.expected_task_runtime, repeat_factor=repeat_factor,
+ scaling_factor=(scaling_factor or DEFAULT_SCALING_FACTOR))
+ return exec_timeout
+
+ def generate_timeout_cmd(
+ self, is_patch: bool, repeat_factor: int, test_timeout_factor: Optional[float] = None,
+ task_timeout_factor: Optional[float] = None, use_default: bool = False) -> TimeoutInfo:
+ """
+ Create the timeout info to use to create a timeout shrub command.
+
+ :param is_patch: Whether the command is being created in a patch build.
+ :param repeat_factor: How many times the suite will be repeated.
+ :param test_timeout_factor: Scaling factor for test timeout.
+ :param task_timeout_factor: Scaling factor for task timeout.
+ :param use_default: Should the default timeout be used.
+ :return: Timeout info for the task.
+ """
+
+ if not self.is_specified or use_default:
+ return TimeoutInfo.default_timeout()
+
+ test_timeout = self.calculate_test_timeout(repeat_factor, test_timeout_factor)
+ task_timeout = self.calculate_task_timeout(repeat_factor, task_timeout_factor)
+
+ if is_patch and (test_timeout > MAX_EXPECTED_TIMEOUT
+ or task_timeout > MAX_EXPECTED_TIMEOUT):
+ frameinfo = getframeinfo(currentframe())
+ LOGGER.error(
+ "This task looks like it is expected to run far longer than normal. This is "
+ "likely due to setting the suite 'repeat' value very high. If you are sure "
+ "this is something you want to do, comment this check out in your patch build "
+ "and resubmit", repeat_value=repeat_factor, timeout=test_timeout,
+ exec_timeout=task_timeout, code_file=frameinfo.filename, code_line=frameinfo.lineno,
+ max_timeout=MAX_EXPECTED_TIMEOUT)
+ raise ValueError("Failing due to expected runtime.")
+
+ return TimeoutInfo.overridden(timeout=test_timeout, exec_timeout=task_timeout)