1 # Copyright 2015 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """A base Model for any kind of alert that can be associated with a bug.""" 6 7 from google.appengine.ext import ndb 8 9 from dashboard.models import internal_only_model 10 from dashboard.models import sheriff as sheriff_module 11 12 13 class Alert(internal_only_model.InternalOnlyModel): 14 """General base class for alerts.""" 15 16 # Whether the alert should only be viewable by internal users. 17 internal_only = ndb.BooleanProperty(indexed=True, default=False) 18 19 # The time the alert fired. 20 timestamp = ndb.DateTimeProperty(indexed=True, auto_now_add=True) 21 22 # Note: -1 denotes an invalid alert and -2 an ignored alert. 23 # By default, this is None, which denotes a non-triaged alert. 24 bug_id = ndb.IntegerProperty(indexed=True) 25 26 # The sheriff rotation that should handle this alert. 27 sheriff = ndb.KeyProperty(kind=sheriff_module.Sheriff, indexed=True) 28 29 # Each Alert is related to one Test. 30 test = ndb.KeyProperty(indexed=True) 31 32 # Each Alert has a revision range it's associated with; however, 33 # start_revision and end_revision could be the same. 34 start_revision = ndb.IntegerProperty(indexed=True) 35 end_revision = ndb.IntegerProperty(indexed=True) 36 37 # The group this alert belongs to. 38 # TODO(qyearsley): If the old AnomalyGroup entities can be removed and 39 # all recent groups have the kind AlertGroup, then the optional argument 40 # kind=alert_group.AlertGroup can be added. 41 group = ndb.KeyProperty(indexed=True) 42 43 def _pre_put_hook(self): 44 """Updates the alert's group.""" 45 # TODO(qyearsley): Extract sub-methods from this method in order 46 # to make it shorter. 47 48 # The group should not be updated if this is the first time that the 49 # the Alert is being put. (If the key is auto-assigned, then key.id() 50 # will be None the first time.) 51 if not self.key.id(): 52 return 53 54 # The previous state of this alert. (If this is the first time the 55 # alert is being put, then this will be None. 56 original_alert = self.key.get(use_cache=False) 57 if original_alert is None: 58 return 59 60 # If the alert does not have a group, don't do anything. 61 if not self.group: 62 return 63 # If the group key is "AnomalyGroup" (the previous incarnation of 64 # AlertGroup), we can just leave it as is. This will only apply to 65 # already-existing Anomaly entities, not new Anomaly entities. 66 if self.group.kind() != 'AlertGroup': 67 self.group = None 68 return 69 group = self.group.get() 70 if not group: 71 return 72 73 # Each AlertGroup should only be associated with entities of one class; 74 # i.e. an Anomaly entity shouldn't be grouped with a StoppageAlert entity. 75 alert_class = self.__class__ 76 77 # When the bug ID changes, this alert may be updated to belong 78 # to the new group. 79 if self.bug_id != original_alert.bug_id: 80 grouped_alerts = alert_class.query( 81 alert_class.group == group.key).fetch() 82 grouped_alerts.append(self) 83 84 # The alert has been assigned a real bug ID. 85 # Update the group bug ID if necessary. 86 if self.bug_id > 0 and group.bug_id != self.bug_id: 87 group.bug_id = self.bug_id 88 group.put() 89 90 # The bug has been marked invalid/ignored. Kick it out of the group. 91 elif self.bug_id < 0 and self.bug_id is not None: 92 self._RemoveFromGroup(grouped_alerts) 93 grouped_alerts.remove(self) 94 95 # The bug has been un-triaged. Update the group's bug ID if this is 96 # the only alert in the group. 97 elif self.bug_id is None and len(grouped_alerts) == 1: 98 group.bug_id = None 99 group.put() 100 101 # Check and update the group's revision range if necessary. 102 group.UpdateRevisionRange(grouped_alerts) 103 104 elif (self.end_revision != original_alert.end_revision or 105 self.start_revision != original_alert.start_revision): 106 grouped_alerts = alert_class.query(alert_class.group == group.key).fetch() 107 grouped_alerts.append(self) 108 group.UpdateRevisionRange(grouped_alerts) 109 110 def _RemoveFromGroup(self, grouped_alerts): 111 """Removes an alert from its group and updates the group's properties. 112 113 Args: 114 grouped_alerts: The list of alerts in |group| used to calculate 115 new revision range; none are modified. 116 """ 117 group = self.group.get() 118 self.group = None 119 grouped_alerts.remove(self) 120 if not grouped_alerts: 121 group.key.delete() 122 return 123 # Update minimum revision range for group. 124 group.UpdateRevisionRange(grouped_alerts) 125 126 127 def _GetTestSuiteFromKey(test_key): 128 """Gets test suite from |test_key|, None if not found.""" 129 pairs = test_key.pairs() 130 if len(pairs) < 3: 131 return None 132 return pairs[2][1] 133 134 135 def GetBotNamesFromAlerts(alerts): 136 """Gets a set with the names of the bots related to some alerts.""" 137 # a.test is a ndb.Key object, and a.test.flat() should return a list like 138 # ['Master', master name, 'Bot', bot name, 'Test', test suite name, ...] 139 return {a.test.flat()[3] for a in alerts} 140