| # Copyright 2020 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from recipe_engine import recipe_api |
| |
| |
| class RetryApi(recipe_api.RecipeApi): |
| """Utilities to retry recipe steps.""" |
| |
| def step( |
| self, |
| step_name, |
| cmd, |
| max_attempts=3, |
| sleep=5.0, |
| backoff_factor=1.5, |
| **kwargs |
| ): |
| """Retry the step with exponential backoff. |
| Args: |
| step_name (str): Name of the step. |
| cmd (None|List[int|string|Placeholder|Path]): The program |
| arguments to run. |
| max_attempts (int): How many times to try before giving up. |
| sleep (int or float): The initial time to sleep between attempts. |
| backoff_factor (int or float): The factor by which the sleep time |
| will be multiplied after each attempt. |
| |
| Returns a `step_data.StepData` for the running step. |
| """ |
| for attempt in range(max_attempts): |
| step = self.m.step(step_name, cmd, ok_ret='any', **kwargs) |
| # Show syslog and emulator_log for FEMU test suite. |
| if 'Run FEMU Test Suite' in step_name: |
| step.presentation.logs['syslog'] = step.raw_io.output_texts['syslog'] |
| step.presentation.logs['emulator_log'] = step.raw_io.output_texts[ |
| 'emulator_log'] |
| if step.retcode != 0: |
| if attempt == max_attempts - 1: |
| step.presentation.status = self.m.step.FAILURE |
| raise self.m.step.StepFailure('.'.join(step.name_tokens), step) |
| self.m.time.sleep(sleep) |
| sleep *= backoff_factor |
| else: |
| # Append an extra step to reflect test flakiness, so that we can easily |
| # collect flaky test statistics. This can also be used to trigger |
| # notification when a flake happens. |
| # This is mainly used for Engine builders for now. |
| if attempt > 0 and 'test:' in step_name: |
| self.m.test_utils.flaky_step(step_name) |
| return step |
| |
| def wrap( |
| self, |
| func, |
| step_name=None, |
| max_attempts=3, |
| sleep=5.0, |
| backoff_factor=1.5, |
| retriable_codes='any', |
| **kwargs |
| ): |
| """Retry wrapped function which needs step support. |
| Args: |
| step_name (str): Name of the step. |
| func (callable): A function that performs the action that should be |
| retried on failure. If it raises a `StepFailure`, it will be retried. |
| Any other exception will end the retry loop and bubble up. |
| retriable_codes ('any' or Tuple(int)): Return codes that should allow a |
| retry. Pass 'any' to accept any. |
| max_attempts (int): How many times to try before giving up. |
| sleep (int or float): The initial time to sleep between attempts. |
| backoff_factor (int or float): The factor by which the sleep time |
| will be multiplied after each attempt. |
| Returns: |
| The result of executing func. |
| """ |
| for attempt in range(max_attempts): |
| try: |
| result = func() |
| # Append an extra step to reflect test flakiness, so that we can easily |
| # collect flaky test statistics. This can also be used to trigger |
| # notification when a flake happens. |
| # This is mainly used for Engine builders for now. |
| if attempt > 0 and step_name is not None and 'test:' in step_name: |
| self.m.test_utils.flaky_step(step_name) |
| return result |
| except self.m.step.StepFailure: |
| # Retrying with nested steps is not supported with retriable codes |
| # different than any. |
| retcode = 0 |
| if retriable_codes != 'any': |
| step = self.m.step.active_result |
| retcode = step.retcode |
| |
| retriable_failure = retriable_codes == 'any' or \ |
| retcode in retriable_codes |
| if not retriable_failure or attempt == max_attempts - 1: |
| raise |
| self.m.time.sleep(sleep) |
| sleep *= backoff_factor |
| |
| def basic_wrap( |
| self, |
| func, |
| max_attempts=3, |
| sleep=5.0, |
| backoff_factor=1.5, |
| timeout=0, |
| **kwargs |
| ): |
| """Retry basic wrapped function without step support. |
| Args: |
| func (callable): A function that performs the action that should be |
| retried on failure. If it raises a `StepFailure` or `InfraFailure`, |
| it will be retried. Any other exception will end the retry loop and |
| bubble up. |
| max_attempts (int): How many times to try before giving up. |
| sleep (int or float): The initial time to sleep between attempts. |
| backoff_factor (int or float): The factor by which the sleep time |
| will be multiplied after each attempt. |
| timeout (int or float): A value passed to the `func` argument. Is |
| multiplied by the `backoff_factor` after each attempt. |
| Returns: |
| The result of executing func. |
| """ |
| for attempt in range(max_attempts): |
| try: |
| result = func(timeout=timeout) |
| return result |
| except (self.m.step.StepFailure, self.m.step.InfraFailure): |
| if attempt == max_attempts - 1: |
| raise |
| self.m.time.sleep(sleep) |
| sleep *= backoff_factor |
| timeout *= backoff_factor |
| |
| def run_flutter_doctor(self): |
| self.step( |
| 'flutter doctor', |
| ['flutter', 'doctor', '--verbose'], |
| max_attempts=3, |
| timeout=300, |
| ) |