| OLD | NEW |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import logging |
| 5 | 6 |
| 6 def ScheduleAnalysisForFlake( | 7 from common import constants |
| 7 _request, _user_email, _is_admin): # pragma: no cover. | 8 from model.flake.flake_analysis_request import FlakeAnalysisRequest |
| 9 from waterfall.flake import initialize_flake_pipeline |
| 10 from waterfall.flake import step_mapper |
| 11 |
| 12 |
| 13 def _CheckFlakeSwarmedAndSupported(request): |
| 14 """Checks if the flake is Swarmed and supported in any build step. |
| 15 |
| 16 Args: |
| 17 request (FlakeAnalysisRequest): The request to analyze a flake. |
| 18 |
| 19 Returns: |
| 20 (swarmed, supported, build_step) |
| 21 swarmed(bool): True if any step is Swarmed. |
| 22 supported(bool): True if any step is supported (Swarmed Gtest). |
| 23 build_step(BuildStep): The representative step that is Swarmed Gtest. |
| 24 """ |
| 25 build_step = None |
| 26 swarmed = False |
| 27 supported = False |
| 28 for step in request.build_steps: |
| 29 swarmed = swarmed or step.swarmed |
| 30 supported = supported or step.supported |
| 31 if step.supported: |
| 32 build_step = step |
| 33 break |
| 34 return swarmed, supported, build_step |
| 35 |
| 36 |
| 37 def _MergeNewRequestIntoExistingOne(new_request, previous_request): |
| 38 """Merges the new request into the previous request. |
| 39 |
| 40 Args: |
| 41 new_request (FlakeAnalysisRequest): The request to analyze a flake. |
| 42 previous_request (FlakeAnalysisRequest): The previous request in record. |
| 43 |
| 44 Returns: |
| 45 (version_number, build_step) |
| 46 version_number (int): The version of the FlakeAnalysisRequest if a new |
| 47 analysis is needed; otherwise 0. |
| 48 build_step (BuildStep): a BuildStep instance if a new analysis is needed; |
| 49 otherwise None. |
| 50 """ |
| 51 # If no bug is attached to the previous analysis or the new request, or both |
| 52 # are attached to the same bug, start a new analysis with a different |
| 53 # configuration. For a configuration that was analyzed 7 days ago, reset it |
| 54 # to use the new reported step of the same configuration. |
| 55 # TODO: move this setting to config. |
| 56 seconds_n_days = 7 * 24 * 60 * 60 # 7 days. |
| 57 candidate_supported_steps = [] |
| 58 need_updating = False |
| 59 for step in new_request.build_steps: |
| 60 existing_step = None |
| 61 for s in previous_request.build_steps: |
| 62 if (step.master_name == s.master_name and |
| 63 step.builder_name == s.builder_name): |
| 64 existing_step = s |
| 65 break |
| 66 |
| 67 if existing_step: |
| 68 # If last reported flake at the existing step was too long ago, drop it |
| 69 # so that the new one is recorded. |
| 70 time_diff = step.reported_time - existing_step.reported_time |
| 71 if time_diff.total_seconds() > seconds_n_days: |
| 72 previous_request.build_steps.remove(existing_step) |
| 73 existing_step = None |
| 74 |
| 75 if not existing_step: |
| 76 need_updating = True |
| 77 previous_request.build_steps.append(step) |
| 78 if step.supported: |
| 79 candidate_supported_steps.append(step) |
| 80 |
| 81 if not candidate_supported_steps: |
| 82 # Find some existing configuration that is not analyzed yet. |
| 83 for s in previous_request.build_steps: |
| 84 if not s.scheduled and s.supported: |
| 85 candidate_supported_steps.append(s) |
| 86 |
| 87 supported_build_step = None |
| 88 if candidate_supported_steps: |
| 89 supported_build_step = candidate_supported_steps[0] |
| 90 previous_request.swarmed = (previous_request.swarmed or |
| 91 supported_build_step.swarmed) |
| 92 previous_request.supported = True |
| 93 need_updating = True |
| 94 |
| 95 if supported_build_step and not previous_request.is_step: |
| 96 supported_build_step.scheduled = True # This will be analyzed. |
| 97 |
| 98 if not previous_request.bug_id: # No bug was attached before. |
| 99 previous_request.bug_id = new_request.bug_id |
| 100 need_updating = True |
| 101 |
| 102 previous_request.user_emails = sorted( |
| 103 set(previous_request.user_emails + new_request.user_emails)) |
| 104 |
| 105 if need_updating: |
| 106 # TODO: update in a transaction. |
| 107 previous_request.put() |
| 108 |
| 109 if not supported_build_step or previous_request.is_step: |
| 110 # No new analysis if: |
| 111 # 1. All analyzed steps are fresh enough and cover all the steps in the |
| 112 # request. |
| 113 # 2. No representative step is Swarmed Gtest. |
| 114 # 3. The flake is a step-level one. |
| 115 return 0, None |
| 116 |
| 117 return previous_request.version_number, supported_build_step |
| 118 |
| 119 |
| 120 def _CheckForNewAnalysis(request): |
| 121 """Checks if a new analysis is needed for the requested flake. |
| 122 |
| 123 Args: |
| 124 request (FlakeAnalysisRequest): The request to analyze a flake. |
| 125 |
| 126 Returns: |
| 127 (version_number, build_step) |
| 128 version_number (int): The version of the FlakeAnalysisRequest if a new |
| 129 analysis is needed; otherwise 0. |
| 130 build_step (BuildStep): a BuildStep instance if a new analysis is needed; |
| 131 otherwise None. |
| 132 """ |
| 133 previous_request = FlakeAnalysisRequest.GetVersion(key=request.name) |
| 134 if not previous_request or (previous_request.bug_id and request.bug_id and |
| 135 previous_request.bug_id != request.bug_id): |
| 136 # If no existing analysis or last analysis was for a different bug, randomly |
| 137 # pick one configuration for a new analysis. |
| 138 if previous_request: |
| 139 # Make a copy to preserve the version number of previous analysis and |
| 140 # prevent concurrent analyses of the same flake. |
| 141 previous_request.CopyFrom(request) |
| 142 request = previous_request |
| 143 |
| 144 swarmed, supported, supported_build_step = _CheckFlakeSwarmedAndSupported( |
| 145 request) |
| 146 request.swarmed = swarmed |
| 147 request.supported = supported |
| 148 |
| 149 if supported_build_step and not request.is_step: |
| 150 supported_build_step.scheduled = True # This step will be analyzed. |
| 151 |
| 152 # For unsupported or step-level flakes, still save them for monitoring. |
| 153 _, saved = request.Save(retry_on_conflict=False) # Create a new version. |
| 154 |
| 155 if not saved or not supported_build_step or request.is_step: |
| 156 # No new analysis if: |
| 157 # 1. Another analysis was just triggered. |
| 158 # 2. No representative step is Swarmed Gtest. |
| 159 # 3. The flake is a step-level one. |
| 160 return 0, None |
| 161 |
| 162 return request.version_number, supported_build_step |
| 163 else: |
| 164 # If no bug is attached to the previous analysis or the new request, or both |
| 165 # are attached to the same bug, start a new analysis with a different |
| 166 # configuration. For a configuration that was analyzed 7 days ago, reset it |
| 167 # to use the new reported step of the same configuration. |
| 168 # TODO: move this setting to config. |
| 169 return _MergeNewRequestIntoExistingOne(request, previous_request) |
| 170 |
| 171 |
| 172 def _IsAuthorizedUser(user_email): |
| 173 """Returns True if the given user email account is authorized for access.""" |
| 174 return user_email and ( |
| 175 user_email in constants.WHITELISTED_APP_ACCOUNTS or |
| 176 user_email.endswith('@google.com')) |
| 177 |
| 178 |
| 179 def ScheduleAnalysisForFlake(request, user_email, is_admin): |
| 8 """Schedules an analysis on the flake in the given request if needed. | 180 """Schedules an analysis on the flake in the given request if needed. |
| 9 | 181 |
| 10 Args: | 182 Args: |
| 11 request (FlakeAnalysisRequest): The request to analyze a flake. | 183 request (FlakeAnalysisRequest): The request to analyze a flake. |
| 12 user_email (str): The email of the requester. | 184 user_email (str): The email of the requester. |
| 13 is_admin (bool): Whether the requester is an admin. | 185 is_admin (bool): Whether the requester is an admin. |
| 14 | 186 |
| 15 Returns: | 187 Returns: |
| 16 An instance of MasterFlakeAnalysis if an analysis was scheduled; otherwise | 188 True if an analysis was scheduled; False if a new analysis is not needed; |
| 17 None if no analysis was scheduled before and the user has no permission to. | 189 None if the user has no permission to. |
| 18 """ | 190 """ |
| 19 # TODO (stgao): hook up with analysis. | 191 assert len(request.build_steps) > 0, 'At least 1 build step is needed!' |
| 192 |
| 193 if not is_admin and not _IsAuthorizedUser(user_email): |
| 194 return None |
| 195 request.user_emails = [user_email] |
| 196 |
| 197 manually_triggered = user_email.endswith('@google.com') |
| 198 |
| 199 for build_step in request.build_steps: |
| 200 step_mapper.FindMatchingWaterfallStep(build_step) |
| 201 |
| 202 version_number, build_step = _CheckForNewAnalysis(request) |
| 203 if version_number and build_step: |
| 204 # A new analysis is needed. |
| 205 logging.info('A new analysis is needed for: %s', build_step) |
| 206 analysis = initialize_flake_pipeline.ScheduleAnalysisIfNeeded( |
| 207 build_step.wf_master_name, build_step.wf_builder_name, |
| 208 build_step.wf_build_number, build_step.wf_step_name, |
| 209 request.name, allow_new_analysis=True, |
| 210 manually_triggered=manually_triggered, |
| 211 queue_name=constants.WATERFALL_ANALYSIS_QUEUE) |
| 212 if analysis: |
| 213 # TODO: put this in a transaction. |
| 214 request = FlakeAnalysisRequest.GetVersion( |
| 215 key=request.name, version=version_number) |
| 216 request.analyses.append(analysis.key) |
| 217 request.put() |
| 218 logging.info('A new analysis was triggered successfully: %s', |
| 219 analysis.key) |
| 220 return True |
| 221 else: |
| 222 logging.info('But no new analysis was not triggered!') |
| 223 else: |
| 224 logging.info('No new analysis is needed: %s', request) |
| 225 |
| 20 return False | 226 return False |
| OLD | NEW |