class SectionTree(ndb.Model):
    tree = ndb.PickleProperty('t')
    # tree = [
    # 		{
    # 			'key_us' : 'urlsafe_key',
    # 			'name' : 'What we do',
    # 			'pages_names': OrderedDict({
    # 				'page_key_us_1': {
    # 					'en' : 'How it works?',
    # 					'it' : 'Come funziona?'
    # 				},
    # 				'page_key_us_2': {
    # 					'en': 'How it works2?',
    # 					'it': 'Come funziona2?'
    # 				},
    # 			}),
    # 			'subs': [
    #                       {
    # 							'key_us' : 'urlsafe_key',
    # 							'name' : 'Specialized in',
    # 							'pages_names': OrderedDict({}),
    # 							'subs': {...}
    # 			             },
    #                        {...}
    #       },
    # 		{ ... }
    # ]

    ins_timestamp = ndb.DateTimeProperty('i_ts',
                                         auto_now_add=True,
                                         indexed=False)
    upd_timestamp = ndb.DateTimeProperty('u_ts', auto_now=True, indexed=False)

    def __repr__(self):
        return "<SectionTree %s>" % self.key

    def get_id(self):
        return self.key.id()

    @classmethod
    def get_by_key(cls, k):
        return ndb.Key(urlsafe=k).get()
Exemple #2
0
class CustomEventCondition(ndb.Model):
    """Datastore model representing a condition from which to build an ndb.Query.

  Attributes:
    name: str, names a property on the NDB model.
    opsymbol: str, specifies a comparison operator.
    value: pickle, contains the actual value, which could be anything.
  """
    name = ndb.StringProperty(required=True)
    opsymbol = ndb.StringProperty(required=True)
    value = ndb.PickleProperty(required=True)

    def get_filter(self):
        return ndb.query.FilterNode(self.name, self.opsymbol,
                                    _apply_timedelta(self.value))

    def match(self, entity):
        """Determines whether an entity matches the condition.

    This is for manually running a condition on an entity outside an NDB query.

    Args:
      entity: a Datastore entity on which to test a match of this condition.

    Returns:
       True if the entity matches, else False.
    """
        entity_value = getattr(entity, self.name)
        condition_value = _apply_timedelta(self.value)
        if self.opsymbol in ['<', '<=', '>', '>='] and entity_value is None:
            # Unset properties are set to None, and so are treated as 0 in
            # comparisons; we prefer to filter.
            return False
        elif ((self.opsymbol == '<' and entity_value < condition_value)
              or (self.opsymbol == '<=' and entity_value <= condition_value)
              or (self.opsymbol == '==' and entity_value == condition_value)
              or (self.opsymbol == '!=' and entity_value != condition_value)
              or (self.opsymbol == '>' and entity_value > condition_value)
              or (self.opsymbol == '>=' and entity_value >= condition_value)):
            return True
        else:
            return False
Exemple #3
0
class UserProfile(ndb.Expando):
    """
    ``ndb.Expando`` is used to store the user_info object as well as
    any additional information specific to a strategy.
    """
    _default_indexed = False
    user_info = ndb.JsonProperty(indexed=False, compressed=True)
    credentials = ndb.PickleProperty(indexed=False)

    @classmethod
    def get_or_create(cls, auth_id, user_info, **kwargs):
        """

        """
        profile = cls.get_by_id(auth_id)
        if profile is None:
            profile = cls(id=auth_id)
        profile.user_info = user_info
        profile.populate(**kwargs)
        profile.put()
        return profile
class MultiplayerOptions(ndb.Model):
    ndb_mode = msgprop.EnumProperty(
        NDB_MultiGameType, default=NDB_MultiGameType.SIMUSOLO)
    ndb_shared = msgprop.EnumProperty(NDB_ShareType, repeated=True)

    def get_mode(self): return MultiplayerGameType.from_ndb(self.ndb_mode)

    def set_mode(self, mode):         self.ndb_mode = mode.to_ndb()

    def get_shared(self): return [ShareType.from_ndb(ndb_st)
                               for ndb_st in self.ndb_shared]

    def set_shared(self, shared):    self.ndb_shared = [s.to_ndb() for s in shared]

    mode = property(get_mode, set_mode)
    shared = property(get_shared, set_shared)
    enabled = ndb.BooleanProperty(default=False)
    cloned = ndb.BooleanProperty(default=True)
    hints = ndb.BooleanProperty(default=True)
    teams = ndb.PickleProperty(default={})

    @staticmethod
    def from_url(qparams):
        opts = MultiplayerOptions()
        opts.enabled = int(qparams.get("players", 1)) > 1
        if opts.enabled:
            opts.mode = MultiplayerGameType(qparams.get("sync_mode", "None"))
            opts.cloned = qparams.get("sync_gen") != "disjoint"
            opts.hints = bool(opts.cloned and qparams.get("sync_hints"))
            opts.shared = enums_from_strlist(ShareType, qparams.getall("sync_shared"))

            teamsRaw = qparams.get("teams")
            if teamsRaw and opts.mode == MultiplayerGameType.SHARED and opts.cloned:
                cnt = 1
                teams = {}
                for teamRaw in teamsRaw.split("|"):
                    teams[cnt] = [int(p) for p in teamRaw.split(",")]
                    cnt += 1
                opts.teams = teams
        return opts
Exemple #5
0
class Talk(ndb.Model):
    talk_title = ndb.StringProperty()
    details = ndb.PickleProperty()
    created = ndb.DateTimeProperty(auto_now_add=True)
    directory_listing = ndb.StringProperty()

    def __init__(self, *args, **kwargs):
        super(Talk, self).__init__(*args, **kwargs)
        self.talk_title = ""
        self.directory_listing = "Listed"
        self.details = {}

    def field(self, f):
        if (self.details.has_key(f)):
            return self.details[f]

        return ""

    def field_ascii(self, f):
        return self.field(f).encode('ascii', 'ignore')

    def set_field(self, field, value):
        self.details[field] = value

    @property
    def title(self):
        return self.talk_title

    @title.setter
    def title(self, t):
        self.talk_title = t

    def is_listed(self):
        return "Listed" == self.directory_listing

    def hide_listing(self):
        self.directory_listing = "Not listed"

    def show_listing(self):
        self.directory_listing = "Listed"
class ProcessedNotifications(ndb.Model):
    """Track if we processed the object notifications for a project today."""
    date_project_dict = ndb.PickleProperty()

    @classmethod
    def getInstance(cls):
        """Returns the single instance of this entity."""
        instance_key = ndb.Key(ProcessedNotifications,
                               'ProcessedNotifications')
        instance = instance_key.get()
        if instance is None:
            instance = ProcessedNotifications(key=instance_key)
            instance.put()
        return instance

    @classmethod
    @ndb.transactional
    def processForToday(cls, project):
        """Mark a project as having been processed (alerts/emails sent) today.

        Args:
           project: Name of project to process. Returns: True if we haven't and
           should process. This fuction modifies the map and assumes an email
           will be sent.

        Returns:
           True if the project was not processed today, False otherwise.
        """
        processed = ProcessedNotifications.getInstance()
        if processed.date_project_dict is None:
            processed.date_project_dict = {}
        today = date.today()
        if today not in processed.date_project_dict:
            processed.date_project_dict.clear()
            processed.date_project_dict[today] = []
        if project in processed.date_project_dict[today]:
            return False
        processed.date_project_dict[today].append(project)
        processed.put()
        return True
class KVS(ndb.Model):
    key_str = ndb.StringProperty()
    value = ndb.PickleProperty()
    value_str = ndb.StringProperty()

    @classmethod
    def get(cls, key_str):
        key = str(ndb.Key("KVS", key_str).id())
        res = KVS.get_by_id(key, use_cache=False, use_memcache=False)
        if res is not None:
            return res.value

    @classmethod
    def put(cls, key_str, value):
        key = str(ndb.Key("KVS", key_str).id())
        kvs_pair = KVS.get_or_insert(key,
                                     key_str=key_str,
                                     value=value,
                                     value_str=str(value))
        kvs_pair.value = value
        kvs_pair.value_str = str(value)
        ndb.Model.put(kvs_pair)
class Game(ndb.Model):
    """A Kind for Game, instantiate with Player as parent"""
    name = ndb.StringProperty(required=True)
    seatsAvailable = ndb.IntegerProperty(default=2)
    playerOne = ndb.StringProperty()
    playerTwo = ndb.StringProperty()
    board = ndb.PickleProperty()
    gameCurrentMove = ndb.IntegerProperty(default=0)
    nextPlayer = ndb.StringProperty()
    gameOver = ndb.BooleanProperty(default=False)
    gameWinner = ndb.StringProperty()

    @property
    def _copyGameToForm(self):
        """Copy relevant fields from Game to GameForm."""
        gf = GameForm()
        for field in gf.all_fields():
            if hasattr(self, field.name):
                setattr(gf, field.name, getattr(self, field.name))
            elif field.name == "websafeKey":
                setattr(gf, field.name, self.key.urlsafe())
            elif field.name == "gameBoard":
                setattr(gf, field.name, ' '.join(getattr(self, 'board')))
        gf.check_initialized()
        return gf

    @property
    def _isWon(self):
        """when the tic-tac-toe game comes to a winning connection"""
        return (self.board[0] == self.board[4] == self.board[8] != ''
                or self.board[2] == self.board[4] == self.board[6] != ''
                or self.board[0] == self.board[1] == self.board[2] != ''
                or self.board[3] == self.board[4] == self.board[5] != ''
                or self.board[6] == self.board[7] == self.board[8] != ''
                or self.board[0] == self.board[3] == self.board[6] != ''
                or self.board[1] == self.board[4] == self.board[7] != ''
                or self.board[2] == self.board[5] == self.board[8] != '')
Exemple #9
0
class BasicModel(ndb.Model):
    """Common data/operations for all storage models
    """

    int_field = ndb.IntegerProperty()
    float_field = ndb.FloatProperty()
    boolean_field = ndb.BooleanProperty()
    string_field = ndb.StringProperty()
    text_field = ndb.TextProperty()
    blob_field = ndb.BlobProperty()
    datetime_field = ndb.DateTimeProperty()
    date_field = ndb.DateProperty()
    time_field = ndb.TimeProperty()
    geopt_field = ndb.GeoPtProperty()
    key_field = ndb.KeyProperty()
    blobkey_field = ndb.BlobKeyProperty()
    user_field = ndb.UserProperty()
    single_structured_field = ndb.StructuredProperty(ChildModel)
    repeated_structured_field = ndb.StructuredProperty(ChildModel,
                                                       repeated=True)
    json_field = ndb.JsonProperty()
    pickle_field = ndb.PickleProperty()
    computed_field = ndb.ComputedProperty(
        lambda self: self.string_field.upper())
Exemple #10
0
class Job(ndb.Model):
    """A Pinpoint job."""

    state = ndb.PickleProperty(required=True, compressed=True)

    #####
    # Job arguments passed in through the API.
    #####

    # Request parameters.
    arguments = ndb.JsonProperty(required=True)

    # TODO: The bug id is only used for posting bug comments when a job starts and
    # completes. This probably should not be the responsibility of Pinpoint.
    bug_id = ndb.IntegerProperty()

    comparison_mode = ndb.StringProperty()

    # The Gerrit server url and change id of the code review to update upon
    # completion.
    gerrit_server = ndb.StringProperty()
    gerrit_change_id = ndb.StringProperty()

    # User-provided name of the job.
    name = ndb.StringProperty()

    tags = ndb.JsonProperty()

    # Email of the job creator.
    user = ndb.StringProperty()

    #####
    # Job state generated by running the job.
    #####

    created = ndb.DateTimeProperty(required=True, auto_now_add=True)

    # This differs from "created" since there may be a lag between the time it
    # was queued and when the scheduler actually starts the job.
    started_time = ndb.DateTimeProperty(required=False)

    # Don't use `auto_now` for `updated`. When we do data migration, we need
    # to be able to modify the Job without changing the Job's completion time.
    updated = ndb.DateTimeProperty(required=True, auto_now_add=True)

    started = ndb.BooleanProperty(default=True)
    completed = ndb.ComputedProperty(
        lambda self: self.started and not self.task)
    failed = ndb.ComputedProperty(
        lambda self: bool(self.exception_details_dict))
    running = ndb.ComputedProperty(
        lambda self: self.started and not self.cancelled and self.task and len(
            self.task) > 0)
    cancelled = ndb.BooleanProperty(default=False)
    cancel_reason = ndb.TextProperty()

    # The name of the Task Queue task this job is running on. If it's present, the
    # job is running. The task is also None for Task Queue retries.
    task = ndb.StringProperty()

    # The contents of any Exception that was thrown to the top level.
    # If it's present, the job failed.
    exception = ndb.TextProperty()
    exception_details = ndb.JsonProperty()

    difference_count = ndb.IntegerProperty()

    retry_count = ndb.IntegerProperty(default=0)

    # We expose the configuration as a first-class property of the Job.
    configuration = ndb.ComputedProperty(
        lambda self: self.arguments.get('configuration'))

    # TODO(simonhatch): After migrating all Pinpoint entities, this can be
    # removed.
    # crbug.com/971370
    @classmethod
    def _post_get_hook(cls, key, future):  # pylint: disable=unused-argument
        e = future.get_result()
        if not e:
            return

        if not getattr(e, 'exception_details'):
            e.exception_details = e.exception_details_dict

    # TODO(simonhatch): After migrating all Pinpoint entities, this can be
    # removed.
    # crbug.com/971370
    @property
    def exception_details_dict(self):
        if hasattr(self, 'exception_details'):
            if self.exception_details:
                return self.exception_details

        if hasattr(self, 'exception'):
            exc = self.exception
            if exc:
                return {'message': exc.splitlines()[-1], 'traceback': exc}

        return None

    @classmethod
    def New(cls,
            quests,
            changes,
            arguments=None,
            bug_id=None,
            comparison_mode=None,
            comparison_magnitude=None,
            gerrit_server=None,
            gerrit_change_id=None,
            name=None,
            pin=None,
            tags=None,
            user=None):
        """Creates a new Job, adds Changes to it, and puts it in the Datstore.

    Args:
      quests: An iterable of Quests for the Job to run.
      changes: An iterable of the initial Changes to run on.
      arguments: A dict with the original arguments used to start the Job.
      bug_id: A monorail issue id number to post Job updates to.
      comparison_mode: Either 'functional' or 'performance', which the Job uses
          to figure out whether to perform a functional or performance bisect.
          If None, the Job will not automatically add any Attempts or Changes.
      comparison_magnitude: The estimated size of the regression or improvement
          to look for. Smaller magnitudes require more repeats.
      gerrit_server: Server of the Gerrit code review to update with job
          results.
      gerrit_change_id: Change id of the Gerrit code review to update with job
          results.
      name: The user-provided name of the Job.
      pin: A Change (Commits + Patch) to apply to every Change in this Job.
      tags: A dict of key-value pairs used to filter the Jobs listings.
      user: The email of the Job creator.

    Returns:
      A Job object.
    """
        state = job_state.JobState(quests,
                                   comparison_mode=comparison_mode,
                                   comparison_magnitude=comparison_magnitude,
                                   pin=pin)
        job = cls(state=state,
                  arguments=arguments or {},
                  bug_id=bug_id,
                  comparison_mode=comparison_mode,
                  gerrit_server=gerrit_server,
                  gerrit_change_id=gerrit_change_id,
                  name=name,
                  tags=tags,
                  user=user,
                  started=False,
                  cancelled=False)

        for c in changes:
            job.AddChange(c)

        job.put()

        # At this point we already have an ID, so we should go through each of the
        # quests associated with the state, and provide the Job ID through a common
        # API.
        job.state.PropagateJob(job)
        job.put()
        return job

    @property
    def job_id(self):
        return '%x' % self.key.id()

    @property
    def status(self):
        if self.failed:
            return 'Failed'

        if self.cancelled:
            return 'Cancelled'

        if self.completed:
            return 'Completed'

        if self.running:
            return 'Running'

        # By default, we assume that the Job is queued.
        return 'Queued'

    @property
    def url(self):
        host = os.environ['HTTP_HOST']
        # TODO(crbug.com/939723): Remove this workaround when not needed.
        if host == 'pinpoint.chromeperf.appspot.com':
            host = 'pinpoint-dot-chromeperf.appspot.com'
        return 'https://%s/job/%s' % (host, self.job_id)

    @property
    def results_url(self):
        if not self.task:
            url = results2.GetCachedResults2(self)
            if url:
                return url
        # Point to the default status page if no results are available.
        return '/results2/%s' % self.job_id

    @property
    def auto_name(self):
        if self.name:
            return self.name

        if self.comparison_mode == job_state.FUNCTIONAL:
            name = 'Functional bisect'
        elif self.comparison_mode == job_state.PERFORMANCE:
            name = 'Performance bisect'
        else:
            name = 'Try job'

        if self.configuration:
            name += ' on ' + self.configuration
            if 'benchmark' in self.arguments:
                name += '/' + self.arguments['benchmark']

        return name

    def AddChange(self, change):
        self.state.AddChange(change)

    def Start(self):
        """Starts the Job and updates it in the Datastore.

    This method is designed to return fast, so that Job creation is responsive
    to the user. It schedules the Job on the task queue without running
    anything. It also posts a bug comment, and updates the Datastore.
    """
        self._Schedule()
        self.started = True
        self.started_time = datetime.datetime.now()
        self.put()

        title = _ROUND_PUSHPIN + ' Pinpoint job started.'
        comment = '\n'.join((title, self.url))
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       send_email=False,
                       _retry_options=RETRY_OPTIONS)

    def _IsTryJob(self):
        return not self.comparison_mode or self.comparison_mode == job_state.TRY

    def _Complete(self):
        logging.debug('Job [%s]: Completed', self.job_id)
        if not self._IsTryJob():
            self.difference_count = len(self.state.Differences())

        try:
            results2.ScheduleResults2Generation(self)
        except taskqueue.Error as e:
            logging.debug('Failed ScheduleResults2Generation: %s', str(e))

        self._FormatAndPostBugCommentOnComplete()
        self._UpdateGerritIfNeeded()
        scheduler.Complete(self)

    def _FormatAndPostBugCommentOnComplete(self):
        if self._IsTryJob():
            # There is no comparison metric.
            title = "<b>%s Job complete. See results below.</b>" % _ROUND_PUSHPIN
            deferred.defer(_PostBugCommentDeferred,
                           self.bug_id,
                           '\n'.join((title, self.url)),
                           _retry_options=RETRY_OPTIONS)
            return

        # There is a comparison metric.
        differences = self.state.Differences()

        if not differences:
            title = "<b>%s Couldn't reproduce a difference.</b>" % _ROUND_PUSHPIN
            deferred.defer(_PostBugCommentDeferred,
                           self.bug_id,
                           '\n'.join((title, self.url)),
                           _retry_options=RETRY_OPTIONS)
            return

        difference_details = []
        authors_with_deltas = {}
        commit_infos = []
        for change_a, change_b in differences:
            if change_b.patch:
                commit_info = change_b.patch.AsDict()
            else:
                commit_info = change_b.last_commit.AsDict()

            values_a = self.state.ResultValues(change_a)
            values_b = self.state.ResultValues(change_b)
            difference = _FormatDifferenceForBug(commit_info, values_a,
                                                 values_b, self.state.metric)
            difference_details.append(difference)
            commit_infos.append(commit_info)
            if values_a and values_b:
                authors_with_deltas[commit_info['author']] = job_state.Mean(
                    values_b) - job_state.Mean(values_a)

        deferred.defer(_UpdatePostAndMergeDeferred,
                       difference_details,
                       commit_infos,
                       authors_with_deltas,
                       self.bug_id,
                       self.tags,
                       self.url,
                       _retry_options=RETRY_OPTIONS)

    def _UpdateGerritIfNeeded(self):
        if self.gerrit_server and self.gerrit_change_id:
            deferred.defer(_UpdateGerritDeferred,
                           self.gerrit_server,
                           self.gerrit_change_id,
                           '%s Job complete.\n\nSee results at: %s' %
                           (_ROUND_PUSHPIN, self.url),
                           _retry_options=RETRY_OPTIONS)

    def Fail(self, exception=None):
        tb = traceback.format_exc() or ''
        title = _CRYING_CAT_FACE + ' Pinpoint job stopped with an error.'
        exc_info = sys.exc_info()
        exc_message = ''
        if exception:
            exc_message = exception
        elif exc_info[1]:
            exc_message = sys.exc_info()[1].message

        self.exception_details = {
            'message': exc_message,
            'traceback': tb,
        }
        self.task = None

        comment = '\n'.join((title, self.url, '', exc_message))
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       _retry_options=RETRY_OPTIONS)
        scheduler.Complete(self)

    def _Schedule(self, countdown=_TASK_INTERVAL):
        # Set a task name to deduplicate retries. This adds some latency, but we're
        # not latency-sensitive. If Job.Run() works asynchronously in the future,
        # we don't need to worry about duplicate tasks.
        # https://github.com/catapult-project/catapult/issues/3900
        task_name = str(uuid.uuid4())
        try:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=countdown)
        except (apiproxy_errors.DeadlineExceededError,
                taskqueue.TransientError):
            raise errors.RecoverableError()

        self.task = task.name

    def _MaybeScheduleRetry(self):
        if not hasattr(self, 'retry_count') or self.retry_count is None:
            self.retry_count = 0

        if self.retry_count >= _MAX_RECOVERABLE_RETRIES:
            return False

        self.retry_count += 1

        # Back off exponentially
        self._Schedule(countdown=_TASK_INTERVAL * (2**self.retry_count))

        return True

    def Run(self):
        """Runs this Job.

    Loops through all Attempts and checks the status of each one, kicking off
    tasks as needed. Does not block to wait for all tasks to finish. Also
    compares adjacent Changes' results and adds any additional Attempts or
    Changes as needed. If there are any incomplete tasks, schedules another
    Run() call on the task queue.
    """
        self.exception_details = None  # In case the Job succeeds on retry.
        self.task = None  # In case an exception is thrown.

        try:
            if not self._IsTryJob():
                self.state.Explore()
            work_left = self.state.ScheduleWork()

            # Schedule moar task.
            if work_left:
                self._Schedule()
            else:
                self._Complete()

            self.retry_count = 0
        except errors.RecoverableError:
            try:
                if not self._MaybeScheduleRetry():
                    self.Fail(errors.RETRY_LIMIT)
            except errors.RecoverableError:
                self.Fail(errors.RETRY_FAILED)
        except BaseException:
            self.Fail()
            raise
        finally:
            # Don't use `auto_now` for `updated`. When we do data migration, we need
            # to be able to modify the Job without changing the Job's completion time.
            self.updated = datetime.datetime.now()

            if self.completed:
                timing_record.RecordJobTiming(self)

            try:
                self.put()
            except (datastore_errors.Timeout,
                    datastore_errors.TransactionFailedError):
                # Retry once.
                self.put()
            except datastore_errors.BadRequestError:
                if self.task:
                    queue = taskqueue.Queue('job-queue')
                    queue.delete_tasks(taskqueue.Task(name=self.task))
                self.task = None

                # The _JobState is too large to fit in an ndb property.
                # Load the Job from before we updated it, and fail it.
                job = self.key.get(use_cache=False)
                job.task = None
                job.Fail()
                job.updated = datetime.datetime.now()
                job.put()
                raise

    def AsDict(self, options=None):
        d = {
            'job_id': self.job_id,
            'configuration': self.configuration,
            'results_url': self.results_url,
            'arguments': self.arguments,
            'bug_id': self.bug_id,
            'comparison_mode': self.comparison_mode,
            'name': self.auto_name,
            'user': self.user,
            'created': self.created.isoformat(),
            'updated': self.updated.isoformat(),
            'difference_count': self.difference_count,
            'exception': self.exception_details_dict,
            'status': self.status,
            'cancel_reason': self.cancel_reason,
        }

        if not options:
            return d

        if OPTION_STATE in options:
            d.update(self.state.AsDict())
        if OPTION_ESTIMATE in options and not self.started:
            d.update(self._GetRunTimeEstimate())
        if OPTION_TAGS in options:
            d['tags'] = {'tags': self.tags}
        return d

    def _GetRunTimeEstimate(self):
        result = timing_record.GetSimilarHistoricalTimings(self)
        if not result:
            return {}

        timings = [t.total_seconds() for t in result.timings]
        return {
            'estimate': {
                'timings': timings,
                'tags': result.tags
            },
            'queue_stats': scheduler.QueueStats(self.configuration)
        }

    def Cancel(self, user, reason):
        # We cannot cancel an already cancelled job.
        if self.cancelled:
            logging.warning(
                'Attempted to cancel a cancelled job "%s"; user = %s, reason = %s',
                self.job_id, user, reason)
            raise errors.CancelError('Job already cancelled.')

        if not scheduler.Cancel(self):
            raise errors.CancelError('Scheduler failed to cancel job.')

        self.cancelled = True
        self.cancel_reason = '{}: {}'.format(user, reason)

        # Remove any "task" identifiers.
        self.task = None
        self.put()

        title = _ROUND_PUSHPIN + ' Pinpoint job cancelled.'
        comment = u'{}\n{}\n\nCancelled by {}, reason given: {}'.format(
            title, self.url, user, reason)
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       send_email=False,
                       _retry_options=RETRY_OPTIONS)
Exemple #11
0
class Job(ndb.Model):
    """A Pinpoint job."""

    created = ndb.DateTimeProperty(required=True, auto_now_add=True)
    updated = ndb.DateTimeProperty(required=True, auto_now=True)

    # The name of the Task Queue task this job is running on. If it's present, the
    # job is running. The task is also None for Task Queue retries.
    task = ndb.StringProperty()

    # The string contents of any Exception that was thrown to the top level.
    # If it's present, the job failed.
    exception = ndb.TextProperty()

    # Request parameters.
    arguments = ndb.JsonProperty(required=True)

    repeat_count = ndb.IntegerProperty(required=True)

    # If True, the service should pick additional Changes to run (bisect).
    # If False, only run the Changes explicitly added by the user.
    auto_explore = ndb.BooleanProperty(required=True)

    # TODO: The bug id is only used for posting bug comments when a job starts and
    # completes. This probably should not be the responsibility of Pinpoint.
    bug_id = ndb.IntegerProperty()

    state = ndb.PickleProperty(required=True)

    @classmethod
    def New(cls,
            arguments,
            quests,
            auto_explore,
            repeat_count=_DEFAULT_REPEAT_COUNT,
            bug_id=None):
        repeat_count = repeat_count or _DEFAULT_REPEAT_COUNT
        # Create job.
        return cls(arguments=arguments,
                   auto_explore=auto_explore,
                   repeat_count=repeat_count,
                   bug_id=bug_id,
                   state=_JobState(quests, repeat_count))

    @property
    def job_id(self):
        return '%x' % self.key.id()

    @property
    def status(self):
        if self.task:
            return 'Running'

        if self.exception:
            return 'Failed'

        return 'Completed'

    @property
    def url(self):
        return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id)

    def AddChange(self, change):
        self.state.AddChange(change)

    def Start(self):
        self.Schedule()
        self._PostBugComment('started', send_email=False)

    def Complete(self):
        self._PostBugComment('completed', include_differences=True)

    def Fail(self):
        self.exception = traceback.format_exc()
        self._PostBugComment('stopped with an error ' + _CRYING_CAT_FACE)

    def Schedule(self):
        # Set a task name to deduplicate retries. This adds some latency, but we're
        # not latency-sensitive. If Job.Run() works asynchronously in the future,
        # we don't need to worry about duplicate tasks.
        # https://github.com/catapult-project/catapult/issues/3900
        task_name = str(uuid.uuid4())
        try:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=_TASK_INTERVAL)
        except apiproxy_errors.DeadlineExceededError:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=_TASK_INTERVAL)

        self.task = task.name

    def Run(self):
        self.exception = None  # In case the Job succeeds on retry.
        self.task = None  # In case an exception is thrown.

        try:
            if self.auto_explore:
                self.state.Explore()
            work_left = self.state.ScheduleWork()

            # Schedule moar task.
            if work_left:
                self.Schedule()
            else:
                self.Complete()
        except BaseException:
            self.Fail()
            raise

    def AsDict(self, include_state=True):
        d = {
            'job_id': self.job_id,
            'arguments': self.arguments,
            'auto_explore': self.auto_explore,
            'bug_id': self.bug_id,
            'created': self.created.isoformat(),
            'updated': self.updated.isoformat(),
            'exception': self.exception,
            'status': self.status,
        }
        if include_state:
            d.update(self.state.AsDict())
        return d

    def _PostBugComment(self,
                        status,
                        include_differences=False,
                        send_email=True):
        if not self.bug_id:
            return

        title = '%s Pinpoint job %s.' % (_ROUND_PUSHPIN, status)
        header = '\n'.join((title, self.url))

        change_details = []
        if include_differences:
            # Include list of Changes.
            differences = tuple(self.state.Differences())
            if differences:
                if len(differences) == 1:
                    change_details.append(
                        '<b>Found significant differences after 1 commit:</b>')
                else:
                    change_details.append(
                        '<b>Found significant differences after each of %d commits:</b>'
                        % len(differences))
                for _, change in differences:
                    change_details.append(_FormatChangeForBug(change))
            else:
                change_details.append(
                    "<b>Couldn't reproduce a difference.</b>")

        comment = '\n\n'.join([header] + change_details)

        issue_tracker = issue_tracker_service.IssueTrackerService(
            utils.ServiceAccountHttp())
        issue_tracker.AddBugComment(self.bug_id,
                                    comment,
                                    send_email=send_email)
class DemoUserInfo(ndb.Model):
    asof = ndb.DateTimeProperty(auto_now_add=True)
    user_id = ndb.StringProperty()
    email = ndb.StringProperty()
    nickname = ndb.StringProperty()
    ds_key = ndb.StringProperty()
    fetching = ndb.IntegerProperty(default = 0)
    calculating = ndb.IntegerProperty(default = 0)
    calc_done = ndb.BooleanProperty(default = False)
    tweets = ndb.PickleProperty(default = [])
    calc_stats = ndb.TextProperty()

    def filename(self):
        return 'user_id: {user_id}, email: {email}, nickname: {nickname}, asof: {asof}' \
            .format(asof = self.asof.isoformat()[:19], user_id = self.user_id, email = self.email, nickname = self.nickname)

    @ndb.transactional
    def indicate_fetch_begun(self):
        key = self.key
        ent = key.get()
        ent.fetching += 1
        ent.put()
        return ent

    @ndb.transactional
    def indicate_fetch_ended(self):
        key = self.key
        ent = key.get()
        ent.fetching -= 1
        ent.put()
        return ent

    @ndb.transactional
    def indicate_calc_begun(self):
        key = self.key
        ent = key.get()
        if 0 == ent.calculating:
            ent.calc_stats = json.dumps(dict())
        ent.calculating += 1
        ent.calc_done = False
        ent.put()
        return ent

    @ndb.transactional
    def indicate_calc_ended(self, batch_stats):
        key = self.key
        ent = key.get()
        ent.calculating -= 1
#         logging.debug('%d', self.calculating)
        if 0 == ent.calculating:
            ent.calc_done = True
        calc_stats = json.loads(ent.calc_stats)
        for stat in batch_stats:
            if stat not in calc_stats.keys():
                calc_stats[stat] = 0.0
            calc_stats[stat] += batch_stats[stat]
        ent.calc_stats = json.dumps(calc_stats)
        ent.put()
        logging.info('<indicate_calc_ended %d %s %s/>', ent.calculating, ent.calc_done, ent.calc_stats)
        return ent
    
    @ndb.transactional
    def extend_tweets(self, tweets):
        key = self.key
        ent = key.get()
        ent.tweets.extend(tweets)
        ent.put()
        return ent

    @ndb.transactional
    def set_ds_key(self, ds_key):
        key = self.key
        ent = key.get()
        if not ent.ds_key:
            ent.ds_key = ds_key
            ent.put()
        else:
            if ds_key != ent.ds_key:
                logging.error('changing ds_key from %s to %s? Makes no sense!', ent.ds_key, ds_key)
        return ent

    @classmethod
    def latest_for_user(cls, user):
        if not user:
            return None
        dui = cls.query(cls.user_id == user.user_id()).order(-cls.asof).get()
        return dui

    def purge(self):
        if self.ds_key:
            matrix = Matrix.find(ds_key = self.ds_key)
            if matrix:
                matrix.purge()
Exemple #13
0
class Job(ndb.Model):
    """A Pinpoint job."""

    state = ndb.PickleProperty(required=True, compressed=True)

    #####
    # Job arguments passed in through the API.
    #####

    # Request parameters.
    arguments = ndb.JsonProperty(required=True)
    bug_id = ndb.IntegerProperty()
    comparison_mode = ndb.StringProperty()

    # The Gerrit server url and change id of the code review to update upon
    # completion.
    gerrit_server = ndb.StringProperty()
    gerrit_change_id = ndb.StringProperty()

    # User-provided name of the job.
    name = ndb.StringProperty()
    tags = ndb.JsonProperty()

    # Email of the job creator.
    user = ndb.StringProperty()

    created = ndb.DateTimeProperty(required=True, auto_now_add=True)

    # This differs from "created" since there may be a lag between the time it
    # was queued and when the scheduler actually starts the job.
    started_time = ndb.DateTimeProperty(required=False)

    # Don't use `auto_now` for `updated`. When we do data migration, we need
    # to be able to modify the Job without changing the Job's completion time.
    updated = ndb.DateTimeProperty(required=True, auto_now_add=True)
    started = ndb.BooleanProperty(default=True)
    completed = ndb.ComputedProperty(lambda self: self.done or (
        not self.use_execution_engine and self.started and not self.task))
    failed = ndb.ComputedProperty(
        lambda self: bool(self.exception_details_dict))
    running = ndb.ComputedProperty(IsRunning)
    cancelled = ndb.BooleanProperty(default=False)
    cancel_reason = ndb.TextProperty()

    # Because of legacy reasons, `completed` is not exactly representative of
    # whether a job is "done" from the execution engine's point of view. We're
    # introducing this top-level property as a definitive flag, transactionally
    # enforced, to signal whether we are done executing a job.
    done = ndb.BooleanProperty(default=False)

    # The name of the Task Queue task this job is running on. If it's present, the
    # job is running. The task is also None for Task Queue retries.
    task = ndb.StringProperty()

    # The contents of any Exception that was thrown to the top level.
    # If it's present, the job failed.
    exception = ndb.TextProperty()
    exception_details = ndb.JsonProperty()
    difference_count = ndb.IntegerProperty()
    retry_count = ndb.IntegerProperty(default=0)

    # We expose the configuration as a first-class property of the Job.
    configuration = ndb.ComputedProperty(
        lambda self: self.arguments.get('configuration'))

    # Pull out the benchmark, chart, and statistic as a structured property at the
    # top-level, so that we can analyse these in a structured manner.
    benchmark_arguments = ndb.StructuredProperty(BenchmarkArguments)

    # Indicate whether we should evaluate this job's tasks through the execution
    # engine.
    use_execution_engine = ndb.BooleanProperty(default=False, indexed=False)

    # TODO(simonhatch): After migrating all Pinpoint entities, this can be
    # removed.
    # crbug.com/971370
    @classmethod
    def _post_get_hook(cls, key, future):  # pylint: disable=unused-argument
        e = future.get_result()
        if not e:
            return

        if not getattr(e, 'exception_details'):
            e.exception_details = e.exception_details_dict

    # TODO(simonhatch): After migrating all Pinpoint entities, this can be
    # removed.
    # crbug.com/971370
    @property
    def exception_details_dict(self):
        if hasattr(self, 'exception_details'):
            if self.exception_details:
                return self.exception_details

        if hasattr(self, 'exception'):
            exc = self.exception
            if exc:
                return {'message': exc.splitlines()[-1], 'traceback': exc}

        return None

    @classmethod
    def New(cls,
            quests,
            changes,
            arguments=None,
            bug_id=None,
            comparison_mode=None,
            comparison_magnitude=None,
            gerrit_server=None,
            gerrit_change_id=None,
            name=None,
            pin=None,
            tags=None,
            user=None,
            use_execution_engine=False):
        """Creates a new Job, adds Changes to it, and puts it in the Datstore.

    Args:
      quests: An iterable of Quests for the Job to run.
      changes: An iterable of the initial Changes to run on.
      arguments: A dict with the original arguments used to start the Job.
      bug_id: A monorail issue id number to post Job updates to.
      comparison_mode: Either 'functional' or 'performance', which the Job uses
        to figure out whether to perform a functional or performance bisect. If
        None, the Job will not automatically add any Attempts or Changes.
      comparison_magnitude: The estimated size of the regression or improvement
        to look for. Smaller magnitudes require more repeats.
      gerrit_server: Server of the Gerrit code review to update with job
        results.
      gerrit_change_id: Change id of the Gerrit code review to update with job
        results.
      name: The user-provided name of the Job.
      pin: A Change (Commits + Patch) to apply to every Change in this Job.
      tags: A dict of key-value pairs used to filter the Jobs listings.
      user: The email of the Job creator.
      use_execution_engine: A bool indicating whether to use the experimental
        execution engine. Currently defaulted to False, but will be switched to
        True and eventually removed as an option later.

    Returns:
      A Job object.
    """
        state = job_state.JobState(quests,
                                   comparison_mode=comparison_mode,
                                   comparison_magnitude=comparison_magnitude,
                                   pin=pin)
        args = arguments or {}
        job = cls(state=state,
                  arguments=args,
                  bug_id=bug_id,
                  comparison_mode=comparison_mode,
                  gerrit_server=gerrit_server,
                  gerrit_change_id=gerrit_change_id,
                  name=name,
                  tags=tags,
                  user=user,
                  started=False,
                  cancelled=False,
                  use_execution_engine=use_execution_engine)

        # Pull out the benchmark arguments to the top-level.
        job.benchmark_arguments = BenchmarkArguments.FromArgs(args)

        if use_execution_engine:
            # Short-circuit the process because we don't need any further processing
            # here when we're using the execution engine.
            job.put()
            return job

        for c in changes:
            job.AddChange(c)

        job.put()

        # At this point we already have an ID, so we should go through each of the
        # quests associated with the state, and provide the Job ID through a common
        # API.
        job.state.PropagateJob(job)
        job.put()
        return job

    def PostCreationUpdate(self):
        title = _ROUND_PUSHPIN + ' Pinpoint job created and queued.'
        pending = 0
        if self.configuration:
            try:
                pending = scheduler.QueueStats(self.configuration).get(
                    'queued_jobs', 0)
            except (scheduler.QueueNotFound, ndb.BadRequestError) as e:
                logging.warning(
                    'Error encountered fetching queue named "%s": %s ',
                    self.configuration, e)

        comment = CREATED_COMMENT_FORMAT.format(
            title=title,
            url=self.url,
            configuration=self.configuration
            if self.configuration else '(None)',
            pending=pending)
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       send_email=True,
                       _retry_options=RETRY_OPTIONS)

    @property
    def job_id(self):
        return '%x' % self.key.id()

    @property
    def status(self):
        if self.failed:
            return 'Failed'

        if self.cancelled:
            return 'Cancelled'

        if self.completed:
            return 'Completed'

        if self.running:
            return 'Running'

        # By default, we assume that the Job is queued.
        return 'Queued'

    @property
    def url(self):
        host = os.environ['HTTP_HOST']
        # TODO(crbug.com/939723): Remove this workaround when not needed.
        if host == 'pinpoint.chromeperf.appspot.com':
            host = 'pinpoint-dot-chromeperf.appspot.com'
        return 'https://%s/job/%s' % (host, self.job_id)

    @property
    def results_url(self):
        if not self.task:
            url = results2.GetCachedResults2(self)
            if url:
                return url
        # Point to the default status page if no results are available.
        return '/results2/%s' % self.job_id

    @property
    def auto_name(self):
        if self.name:
            return self.name

        if self.comparison_mode == job_state.FUNCTIONAL:
            name = 'Functional bisect'
        elif self.comparison_mode == job_state.PERFORMANCE:
            name = 'Performance bisect'
        else:
            name = 'Try job'

        if self.configuration:
            name += ' on ' + self.configuration
            if 'benchmark' in self.arguments:
                name += '/' + self.arguments['benchmark']

        return name

    def AddChange(self, change):
        self.state.AddChange(change)

    def Start(self):
        """Starts the Job and updates it in the Datastore.

    This method is designed to return fast, so that Job creation is responsive
    to the user. It schedules the Job on the task queue without running
    anything. It also posts a bug comment, and updates the Datastore.
    """
        if self.use_execution_engine:
            # Treat this as if it's a poll, and run the handler here.
            try:
                task_module.Evaluate(
                    self,
                    event_module.Event(type='initiate',
                                       target_task=None,
                                       payload={}),
                    task_evaluator.ExecutionEngine(self)),
            except task_module.Error as error:
                logging.error('Failed: %s', error)
                self.Fail()
                self.put()
                return
        else:
            self._Schedule()
        self.started = True
        self.started_time = datetime.datetime.now()
        self.put()

        title = _ROUND_PUSHPIN + ' Pinpoint job started.'
        comment = '\n'.join((title, self.url))
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       send_email=True,
                       _retry_options=RETRY_OPTIONS)

    def _IsTryJob(self):
        return not self.comparison_mode or self.comparison_mode == job_state.TRY

    def _Complete(self):
        logging.debug('Job [%s]: Completed', self.job_id)
        if self.use_execution_engine:
            scheduler.Complete(self)

            # Only proceed if this thread/process is the one that successfully marked
            # a job done.
            if not MarkDone(self.job_id):
                return
            logging.debug('Job [%s]: Marked done', self.job_id)

        if not self._IsTryJob() and not self.use_execution_engine:
            self.difference_count = len(self.state.Differences())

        try:
            # TODO(dberris): Migrate results2 generation to tasks and evaluators.
            results2.ScheduleResults2Generation(self)
        except taskqueue.Error as e:
            logging.debug('Failed ScheduleResults2Generation: %s', str(e))

        self._FormatAndPostBugCommentOnComplete()
        self._UpdateGerritIfNeeded()
        scheduler.Complete(self)

    def _FormatAndPostBugCommentOnComplete(self):
        logging.debug('Processing outputs.')
        if self._IsTryJob():
            # There is no comparison metric.
            title = '<b>%s Job complete. See results below.</b>' % _ROUND_PUSHPIN
            deferred.defer(_PostBugCommentDeferred,
                           self.bug_id,
                           '\n'.join((title, self.url)),
                           labels=['Pinpoint-Tryjob-Completed'],
                           _retry_options=RETRY_OPTIONS)
            return

        # There is a comparison metric.
        differences = []
        result_values = {}
        if not self.use_execution_engine:
            differences = self.state.Differences()
            for change_a, change_b in differences:
                result_values.setdefault(change_a,
                                         self.state.ResultValues(change_a))
                result_values.setdefault(change_b,
                                         self.state.ResultValues(change_b))
        else:
            logging.debug('Execution Engine: Finding culprits.')
            context = task_module.Evaluate(
                self, event_module.SelectEvent(),
                evaluators.Selector(
                    event_type='select',
                    include_keys={'culprits', 'change', 'result_values'}))
            differences = [
                (change_module.ReconstituteChange(change_a),
                 change_module.ReconstituteChange(change_b))
                for change_a, change_b in context.get('performance_bisection',
                                                      {}).get('culprits', [])
            ]
            result_values = {
                change_module.ReconstituteChange(v.get('change')):
                v.get('result_values')
                for v in context.values()
                if 'change' in v and 'result_values' in v
            }

        if not differences:
            title = "<b>%s Couldn't reproduce a difference.</b>" % _ROUND_PUSHPIN
            deferred.defer(_PostBugCommentDeferred,
                           self.bug_id,
                           '\n'.join((title, self.url)),
                           labels=['Pinpoint-No-Repro'],
                           _retry_options=RETRY_OPTIONS)
            return

        # Collect the result values for each of the differences
        difference_details = []
        commit_infos = []
        commits_with_deltas = {}
        for change_a, change_b in differences:
            if change_b.patch:
                commit = change_b.patch
            else:
                commit = change_b.last_commit
            commit_info = commit.AsDict()

            values_a = result_values[change_a]
            values_b = result_values[change_b]
            difference = _FormatDifferenceForBug(commit_info, values_a,
                                                 values_b, self.state.metric)
            difference_details.append(difference)
            commit_infos.append(commit_info)
            if values_a and values_b:
                mean_delta = job_state.Mean(values_b) - job_state.Mean(
                    values_a)
                commits_with_deltas[commit.id_string] = (mean_delta,
                                                         commit_info)

        deferred.defer(_UpdatePostAndMergeDeferred,
                       difference_details,
                       commit_infos,
                       list(commits_with_deltas.values()),
                       self.bug_id,
                       self.tags,
                       self.url,
                       _retry_options=RETRY_OPTIONS)

    def _UpdateGerritIfNeeded(self):
        if self.gerrit_server and self.gerrit_change_id:
            deferred.defer(_UpdateGerritDeferred,
                           self.gerrit_server,
                           self.gerrit_change_id,
                           '%s Job complete.\n\nSee results at: %s' %
                           (_ROUND_PUSHPIN, self.url),
                           _retry_options=RETRY_OPTIONS)

    def Fail(self, exception=None):
        tb = traceback.format_exc() or ''
        title = _CRYING_CAT_FACE + ' Pinpoint job stopped with an error.'
        exc_info = sys.exc_info()
        if exception is None:
            if exc_info[1] is None:
                # We've been called without a exception in sys.exc_info or in our args.
                # This should not happen.
                exception = errors.JobError('Unknown job error')
                exception.category = 'pinpoint'
            else:
                exception = exc_info[1]
        exc_message = exception.message
        category = None
        if isinstance(exception, errors.JobError):
            category = exception.category

        self.exception_details = {
            'message': exc_message,
            'traceback': tb,
            'category': category,
        }
        self.task = None

        comment = '\n'.join((title, self.url, '', exc_message))

        # Short-circuit jobs failure updates when we are not the first one to mark a
        # job done.
        if self.use_execution_engine and not MarkDone(self.job_id):
            return

        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       labels=['Pinpoint-Job-Failed'],
                       send_email=True,
                       _retry_options=RETRY_OPTIONS)
        scheduler.Complete(self)

    def _Schedule(self, countdown=_TASK_INTERVAL):
        # Set a task name to deduplicate retries. This adds some latency, but we're
        # not latency-sensitive. If Job.Run() works asynchronously in the future,
        # we don't need to worry about duplicate tasks.
        # https://github.com/catapult-project/catapult/issues/3900
        task_name = str(uuid.uuid4())
        try:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=countdown)
        except (apiproxy_errors.DeadlineExceededError,
                taskqueue.TransientError) as exc:
            raise errors.RecoverableError(exc)

        self.task = task.name

    def _MaybeScheduleRetry(self):
        if not hasattr(self, 'retry_count') or self.retry_count is None:
            self.retry_count = 0

        if self.retry_count >= _MAX_RECOVERABLE_RETRIES:
            return False

        self.retry_count += 1

        # Back off exponentially
        self._Schedule(countdown=_TASK_INTERVAL * (2**self.retry_count))

        return True

    def Run(self):
        """Runs this Job.

    Loops through all Attempts and checks the status of each one, kicking off
    tasks as needed. Does not block to wait for all tasks to finish. Also
    compares adjacent Changes' results and adds any additional Attempts or
    Changes as needed. If there are any incomplete tasks, schedules another
    Run() call on the task queue.
    """
        self.exception_details = None  # In case the Job succeeds on retry.
        self.task = None  # In case an exception is thrown.

        try:
            if self.use_execution_engine:
                # Treat this as if it's a poll, and run the handler here.
                context = task_module.Evaluate(
                    self,
                    event_module.Event(type='initiate',
                                       target_task=None,
                                       payload={}),
                    task_evaluator.ExecutionEngine(self))
                result_status = context.get('performance_bisection',
                                            {}).get('status')
                if result_status not in {'failed', 'completed'}:
                    return

                if result_status == 'failed':
                    execution_errors = context['find_culprit'].get(
                        'errors', [])
                    if execution_errors:
                        self.exception_details = execution_errors[0]

                self._Complete()
                return

            if not self._IsTryJob():
                self.state.Explore()
            work_left = self.state.ScheduleWork()

            # Schedule moar task.
            if work_left:
                self._Schedule()
            else:
                self._Complete()

            self.retry_count = 0
        except errors.RecoverableError as e:
            try:
                if not self._MaybeScheduleRetry():
                    self.Fail(errors.JobRetryLimitExceededError(wrapped_exc=e))
            except errors.RecoverableError as e:
                self.Fail(errors.JobRetryFailed(wrapped_exc=e))
        except BaseException:
            self.Fail()
            raise
        finally:
            # Don't use `auto_now` for `updated`. When we do data migration, we need
            # to be able to modify the Job without changing the Job's completion time.
            self.updated = datetime.datetime.now()

            if self.completed:
                timing_record.RecordJobTiming(self)

            try:
                self.put()
            except (datastore_errors.Timeout,
                    datastore_errors.TransactionFailedError):
                # Retry once.
                self.put()
            except datastore_errors.BadRequestError:
                if self.task:
                    queue = taskqueue.Queue('job-queue')
                    queue.delete_tasks(taskqueue.Task(name=self.task))
                self.task = None

                # The _JobState is too large to fit in an ndb property.
                # Load the Job from before we updated it, and fail it.
                job = self.key.get(use_cache=False)
                job.task = None
                job.Fail()
                job.updated = datetime.datetime.now()
                job.put()
                raise

    def AsDict(self, options=None):
        d = {
            'job_id': self.job_id,
            'configuration': self.configuration,
            'results_url': self.results_url,
            'arguments': self.arguments,
            'bug_id': self.bug_id,
            'comparison_mode': self.comparison_mode,
            'name': self.auto_name,
            'user': self.user,
            'created': self.created.isoformat(),
            'updated': self.updated.isoformat(),
            'difference_count': self.difference_count,
            'exception': self.exception_details_dict,
            'status': self.status,
            'cancel_reason': self.cancel_reason,
        }

        if not options:
            return d

        if OPTION_STATE in options:
            if self.use_execution_engine:
                d.update(
                    task_module.Evaluate(
                        self,
                        event_module.Event(
                            type='serialize', target_task=None, payload={}),
                        job_serializer.Serializer()) or {})
            else:
                d.update(self.state.AsDict())
        if OPTION_ESTIMATE in options and not self.started:
            d.update(self._GetRunTimeEstimate())
        if OPTION_TAGS in options:
            d['tags'] = {'tags': self.tags}
        return d

    def _GetRunTimeEstimate(self):
        result = timing_record.GetSimilarHistoricalTimings(self)
        if not result:
            return {}

        timings = [t.total_seconds() for t in result.timings]
        return {
            'estimate': {
                'timings': timings,
                'tags': result.tags
            },
            'queue_stats': scheduler.QueueStats(self.configuration)
        }

    def Cancel(self, user, reason):
        # We cannot cancel an already cancelled job.
        if self.cancelled:
            logging.warning(
                'Attempted to cancel a cancelled job "%s"; user = %s, reason = %s',
                self.job_id, user, reason)
            raise errors.CancelError('Job already cancelled.')

        if not scheduler.Cancel(self):
            raise errors.CancelError('Scheduler failed to cancel job.')

        self.cancelled = True
        self.cancel_reason = '{}: {}'.format(user, reason)

        # Remove any "task" identifiers.
        self.task = None
        self.put()
        title = _ROUND_PUSHPIN + ' Pinpoint job cancelled.'
        comment = u'{}\n{}\n\nCancelled by {}, reason given: {}'.format(
            title, self.url, user, reason)
        deferred.defer(_PostBugCommentDeferred,
                       self.bug_id,
                       comment,
                       send_email=True,
                       labels=['Pinpoint-Job-Cancelled'],
                       _retry_options=RETRY_OPTIONS)
Exemple #14
0
class Job(ndb.Model):
    """A Pinpoint job."""

    created = ndb.DateTimeProperty(required=True, auto_now_add=True)
    updated = ndb.DateTimeProperty(required=True, auto_now=True)

    # The name of the Task Queue task this job is running on. If it's present, the
    # job is running. The task is also None for Task Queue retries.
    task = ndb.StringProperty()

    # The string contents of any Exception that was thrown to the top level.
    # If it's present, the job failed.
    exception = ndb.TextProperty()

    # Request parameters.
    arguments = ndb.JsonProperty(required=True)

    repeat_count = ndb.IntegerProperty(required=True)

    # If True, the service should pick additional Changes to run (bisect).
    # If False, only run the Changes explicitly added by the user.
    auto_explore = ndb.BooleanProperty(required=True)

    # TODO: The bug id is only used for posting bug comments when a job starts and
    # completes. This probably should not be the responsibility of Pinpoint.
    bug_id = ndb.IntegerProperty()

    state = ndb.PickleProperty(required=True)

    @classmethod
    def New(cls,
            arguments,
            quests,
            auto_explore,
            repeat_count=_DEFAULT_REPEAT_COUNT,
            bug_id=None):
        repeat_count = repeat_count or _DEFAULT_REPEAT_COUNT
        # Create job.
        return cls(arguments=arguments,
                   auto_explore=auto_explore,
                   repeat_count=repeat_count,
                   bug_id=bug_id,
                   state=_JobState(quests, repeat_count))

    @property
    def job_id(self):
        return '%x' % self.key.id()

    @property
    def status(self):
        if self.task:
            return 'Running'

        if self.exception:
            return 'Failed'

        return 'Completed'

    @property
    def url(self):
        return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id)

    def AddChange(self, change):
        self.state.AddChange(change)

    def Start(self):
        self.Schedule()
        self._PostBugComment('started')

    def Complete(self):
        self._PostBugComment('completed')

    def Fail(self):
        self.exception = traceback.format_exc()
        self._PostBugComment('stopped with an error')

    def Schedule(self):
        task = taskqueue.add(queue_name='job-queue',
                             url='/api/run/' + self.job_id,
                             countdown=_TASK_INTERVAL)
        self.task = task.name

    def Run(self):
        self.exception = None  # In case the Job succeeds on retry.
        self.task = None  # In case an exception is thrown.

        try:
            if self.auto_explore:
                self.state.Explore()
            work_left = self.state.ScheduleWork()

            # Schedule moar task.
            if work_left:
                self.Schedule()
            else:
                self.Complete()
        except BaseException:
            self.Fail()
            raise

    def AsDict(self, include_state=True):
        d = {
            'job_id': self.job_id,
            'arguments': self.arguments,
            'auto_explore': self.auto_explore,
            'created': self.created.isoformat(),
            'updated': self.updated.isoformat(),
            'exception': self.exception,
            'status': self.status,
        }
        if include_state:
            d.update(self.state.AsDict())
        return d

    def _PostBugComment(self, status):
        if not self.bug_id:
            return

        title = '%s Pinpoint job %s.' % (_ROUND_PUSHPIN, status)
        header = '\n'.join((title, self.url))

        # Include list of Changes.
        change_details = []
        for _, change in self.state.Differences():
            # TODO: Store the commit info in the Commit.
            commit = change.last_commit
            commit_info = gitiles_service.CommitInfo(commit.repository_url,
                                                     commit.git_hash)
            subject = commit_info['message'].split('\n', 1)[0]
            author = commit_info['author']['email']
            time = commit_info['committer']['time']

            byline = 'By %s %s %s' % (author, _MIDDLE_DOT, time)
            git_link = commit.repository + '@' + commit.git_hash
            change_details.append('\n'.join((subject, byline, git_link)))

        comment = '\n\n'.join([header] + change_details)

        issue_tracker = issue_tracker_service.IssueTrackerService(
            utils.ServiceAccountHttp())
        issue_tracker.AddBugComment(self.bug_id, comment, send_email=False)
Exemple #15
0
class DatastoreCacheModel(ndb.Model):
    data = ndb.PickleProperty(indexed=False, compressed=True)
    expires = ndb.DateTimeProperty(indexed=False)
Exemple #16
0
class OrderChangeLogEntry(ndb.Model):
    what = ndb.StringProperty(indexed=False)
    old = ndb.PickleProperty()
    new = ndb.PickleProperty()
Exemple #17
0
class Job(ndb.Model):
  """A Pinpoint job."""

  created = ndb.DateTimeProperty(required=True, auto_now_add=True)
  updated = ndb.DateTimeProperty(required=True, auto_now=True)

  # The name of the Task Queue task this job is running on. If it's present, the
  # job is running. The task is also None for Task Queue retries.
  task = ndb.StringProperty()

  # The string contents of any Exception that was thrown to the top level.
  # If it's present, the job failed.
  exception = ndb.StringProperty()

  # Request parameters.
  configuration = ndb.StringProperty(required=True)
  test_suite = ndb.StringProperty()
  test = ndb.StringProperty()
  metric = ndb.StringProperty()

  # If True, the service should pick additional Changes to run (bisect).
  # If False, only run the Changes explicitly added by the user.
  auto_explore = ndb.BooleanProperty(required=True)

  # TODO: The bug id is only used for posting bug comments when a job starts and
  # completes. This probably should not be the responsibility of Pinpoint.
  bug_id = ndb.IntegerProperty()

  state = ndb.PickleProperty(required=True)

  @classmethod
  def New(cls, configuration, test_suite, test, metric, auto_explore, bug_id):
    # Get list of quests.
    quests = [quest_module.FindIsolate(configuration)]
    if test_suite:
      quests.append(quest_module.RunTest(configuration, test_suite, test))
    if metric:
      quests.append(quest_module.ReadValue(metric, test))

    # Create job.
    return cls(
        configuration=configuration,
        test_suite=test_suite,
        test=test,
        metric=metric,
        auto_explore=auto_explore,
        bug_id=bug_id,
        state=_JobState(quests, _DEFAULT_MAX_ATTEMPTS))

  @property
  def job_id(self):
    return self.key.urlsafe()

  @property
  def status(self):
    if self.task:
      return 'Running'

    if self.exception:
      return 'Failed'

    return 'Completed'

  @property
  def url(self):
    return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id)

  def AddChange(self, change):
    self.state.AddChange(change)

  def Start(self):
    self.Schedule()

    comment = 'Pinpoint job started.\n' + self.url
    issue_tracker = issue_tracker_service.IssueTrackerService(
        utils.ServiceAccountHttp())
    issue_tracker.AddBugComment(self.bug_id, comment, send_email=False)

  def Complete(self):
    comment = 'Pinpoint job complete!\n' + self.url
    issue_tracker = issue_tracker_service.IssueTrackerService(
        utils.ServiceAccountHttp())
    issue_tracker.AddBugComment(self.bug_id, comment, send_email=False)

  def Schedule(self):
    task = taskqueue.add(queue_name='job-queue', url='/api/run/' + self.job_id,
                         countdown=_TASK_INTERVAL)
    self.task = task.name

  def Run(self):
    self.exception = None  # In case the Job succeeds on retry.
    self.task = None  # In case an exception is thrown.

    try:
      if self.auto_explore:
        self.state.Explore()
      work_left = self.state.ScheduleWork()

      # Schedule moar task.
      if work_left:
        self.Schedule()
      else:
        self.Complete()
    except BaseException as e:
      self.exception = str(e)
      raise

  def AsDict(self):
    return {
        'job_id': self.job_id,

        'configuration': self.configuration,
        'test_suite': self.test_suite,
        'test': self.test,
        'metric': self.metric,
        'auto_explore': self.auto_explore,

        'created': self.created.strftime('%Y-%m-%d %H:%M:%S %Z'),
        'updated': self.updated.strftime('%Y-%m-%d %H:%M:%S %Z'),
        'status': self.status,

        'state': self.state.AsDict(),
    }
Exemple #18
0
class History(ndb.Model):
    move_date = ndb.DateTimeProperty(required=True, auto_now_add=True)
    user = ndb.KeyProperty(required=True, kind='User')
    column = ndb.IntegerProperty(required=True)
    board_state_after_move = ndb.PickleProperty(required=True)
Exemple #19
0
class Person(ndb.Model):
    chat_id = ndb.IntegerProperty()
    name = ndb.StringProperty()
    last_name = ndb.StringProperty()
    username = ndb.StringProperty()
    state = ndb.IntegerProperty(default=-1, indexed=True)
    location = ndb.GeoPtProperty()
    last_state = ndb.IntegerProperty()
    last_mod = ndb.DateTimeProperty(auto_now=True)
    search_count = ndb.IntegerProperty(default=0)
    vote_reminder_enabled = ndb.BooleanProperty(default=True)
    search_radius = ndb.IntegerProperty(default=1)  #km
    search_type = ndb.StringProperty(
        default=parameters.SEARCH_TYPE_DRINKING_WATER)
    search_locations = ndb.PickleProperty()
    enabled = ndb.BooleanProperty(default=True)

    def getFirstName(self, escapeMarkdown=True):
        if escapeMarkdown:
            return utility.escapeMarkdown(self.name.encode('utf-8'))
        return self.name.encode('utf-8')

    def getLastName(self, escapeMarkdown=True):
        if escapeMarkdown:
            return utility.escapeMarkdown(self.last_name.encode('utf-8'))
        return self.last_name.encode('utf-8')

    def getUsername(self):
        return self.username.encode('utf-8') if self.username else None

    def getUserInfoString(self, escapeMarkdown=True):
        info = self.getFirstName(escapeMarkdown)
        if self.last_name:
            info += ' ' + self.getLastName(escapeMarkdown)
        if self.username:
            info += ' @' + self.getUsername()
        info += ' ({})'.format(self.chat_id)
        return info

    def setEnabled(self, enabled, put=False):
        self.enabled = enabled
        if put:
            self.put()

    def updateUsername(self, username, put=False):
        if (self.username != username):
            self.username = username
            if put:
                self.put()

    def setState(self, newstate, put=True):
        self.last_state = self.state
        self.state = newstate
        if put:
            self.put()

    def isAdministrator(self):
        result = self.chat_id in key.AMMINISTRATORI_ID
        #logging.debug("Amministratore: " + str(result))
        return result

    def setLocation(self, latitude, longitude, put):
        self.location = ndb.GeoPt(latitude, longitude)
        if put:
            self.put()

    def getSearchType(self):
        return self.search_type.encode('utf-8')

    def getNextSearchRadius(self):
        nextRadiusIndex = parameters.POSSIBLE_RADII.index(
            self.search_radius) + 1
        if nextRadiusIndex < len(parameters.POSSIBLE_RADII):
            return parameters.POSSIBLE_RADII[nextRadiusIndex]
        return None

    def isAdmin(self):
        return self.chat_id in key.AMMINISTRATORI_ID

    def increaseSearchCount(self, put=False):
        self.search_count += 1
        if put:
            self.put()

    def isTimeToRemindToVote(self):
        return self.vote_reminder_enabled \
               and \
               self.search_count % parameters.REMIND_VOTE_BOT_EVERY_N_SEARCH == 0
Exemple #20
0
class Game(ndb.Model):
    """Game class"""
    player1 = ndb.KeyProperty(required=True, kind='User')
    player2 = ndb.KeyProperty(required=True, kind='User')
    player1Colour = ndb.StringProperty(required=True)
    player2Colour = ndb.StringProperty(required=True)
    whose_turn = ndb.KeyProperty(required=True, kind='User')
    created = ndb.DateTimeProperty(auto_now_add=True)
    # Board is 7x6 (width x height), so 42 holes/spaces
    board = ndb.PickleProperty(required=True)
    holes_remaining = ndb.IntegerProperty(required=True, default=42)
    game_over = ndb.BooleanProperty(required=True, default=False)
    history = ndb.StructuredProperty(History, repeated=True)

    @classmethod
    def new_game(cls, user1, user2):
        """Creates and returns a new game"""
        # Check if users exist
        player1 = User.query(User.name == user1).get()
        if not player1:
            raise ValueError('Player 1 does not exist')

        player2 = User.query(User.name == user2).get()
        if not player2:
            raise ValueError('Player 2 does not exist')

        # Randomly choose which player plays first and set token colours
        if random.choice(range(1, 3)) == 1:
            whose_turn = player1.key
            player1_colour = 'R'
            player2_colour = "Y"
        else:
            whose_turn = player2.key
            player1_colour = 'Y'
            player2_colour = "R"

        # Create board, game and put in datastore
        game = Game(player1=player1.key,
                    player2=player2.key,
                    player1Colour=player1_colour,
                    player2Colour=player2_colour,
                    whose_turn=whose_turn,
                    board=Board())
        game.put()
        return game

    def switch_turn(self):
        """Swaps who's turn it is in the game"""
        if self.whose_turn == self.player1:
            self.whose_turn = self.player2
        else:
            self.whose_turn = self.player1

    def is_won(self):
        """Checks if the game has been won"""
        return self.board.is_won()

    def end_game(self, won):
        self.game_over = True
        if won:

            # get the loser (Winner is in whose_turn)
            if self.player1 == self.whose_turn:
                loser = self.player2
            else:
                loser = self.player1

            # Add the game to the score 'board'
            score = Score(winning_user=self.whose_turn,
                          losing_user=loser,
                          date=date.today(),
                          holes_remaining=self.holes_remaining)
            score.put()

            # Update winning user rank
            user_rank = UserRank.query(
                UserRank.user_name == self.whose_turn).get()
            if not user_rank:
                user_rank = UserRank()
                user_rank.user_name = self.whose_turn
                user_rank.games_played = 1
                user_rank.games_won = 1
            else:
                user_rank.games_played += 1
                user_rank.games_won += 1
            user_rank.win_ratio = user_rank.games_won / (
                user_rank.games_played + 0.0)
            user_rank.put()

            # Update losing user rank
            user_rank = UserRank.query(UserRank.user_name == loser).get()
            if not user_rank:
                user_rank = UserRank()
                user_rank.user_name = loser
                user_rank.games_played = 1
                user_rank.games_won = 0
            else:
                user_rank.games_played += 1

            user_rank.win_ratio = user_rank.games_won / (
                user_rank.games_played + 0.0)
            user_rank.put()

    def to_form(self, message):
        """Returns a GameForm representation of the Game"""
        form = GameForm()
        form.urlsafe_key = self.key.urlsafe()
        form.player1 = self.player1.get().name
        form.player2 = self.player2.get().name
        # TODO: return player colours
        form.whose_turn = self.whose_turn.get().name
        form.holes_remaining = self.holes_remaining
        form.game_over = self.game_over
        form.message = message
        form.board = str(self.board.board)
        return form

    def history_to_form(self):
        """Returns a series of History Forms"""
        forms = HistoryForms()
        for history in self.history:
            form = HistoryForm()
            form.move_date = str(history.move_date)
            form.user_name = history.user.get().name
            form.column = history.column
            print str(history.board_state_after_move)
            form.board_state = str(history.board_state_after_move)
            forms.items.append(form)
        return forms
Exemple #21
0
class Job(ndb.Model):
  """A Pinpoint job."""

  created = ndb.DateTimeProperty(required=True, auto_now_add=True)
  # Don't use `auto_now` for `updated`. When we do data migration, we need
  # to be able to modify the Job without changing the Job's completion time.
  updated = ndb.DateTimeProperty(required=True, auto_now_add=True)

  # The name of the Task Queue task this job is running on. If it's present, the
  # job is running. The task is also None for Task Queue retries.
  task = ndb.StringProperty()

  # The string contents of any Exception that was thrown to the top level.
  # If it's present, the job failed.
  exception = ndb.TextProperty()

  # Request parameters.
  arguments = ndb.JsonProperty(required=True)

  # If True, the service should pick additional Changes to run (bisect).
  # If False, only run the Changes explicitly added by the user.
  auto_explore = ndb.BooleanProperty(required=True)

  # TODO: The bug id is only used for posting bug comments when a job starts and
  # completes. This probably should not be the responsibility of Pinpoint.
  bug_id = ndb.IntegerProperty()

  state = ndb.PickleProperty(required=True, compressed=True)

  @classmethod
  def New(cls, arguments, quests, auto_explore, bug_id=None):
    # Create job.
    return cls(
        arguments=arguments,
        auto_explore=auto_explore,
        bug_id=bug_id,
        state=_JobState(quests))

  @property
  def job_id(self):
    return '%x' % self.key.id()

  @property
  def status(self):
    if self.task:
      return 'Running'

    if self.exception:
      return 'Failed'

    return 'Completed'

  @property
  def url(self):
    return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id)

  def AddChange(self, change):
    self.state.AddChange(change)

  def Start(self):
    self._Schedule()

    title = _ROUND_PUSHPIN + ' Pinpoint job started.'
    comment = '\n'.join((title, self.url))
    self._PostBugComment(comment, send_email=False)

  def _Complete(self):
    # Format bug comment.
    differences = tuple(self.state.Differences())

    if not differences:
      title = "<b>%s Couldn't reproduce a difference.</b>" % _ROUND_PUSHPIN
      self._PostBugComment('\n'.join((title, self.url)))
      return

    # Include list of Changes.
    owner = None
    cc_list = set()
    commit_details = []
    for _, change in differences:
      commit_info = change.last_commit.Details()

      author = commit_info['author']['email']
      owner = author  # TODO: Assign the largest difference, not the last one.
      cc_list.add(author)
      cc_list |= frozenset(re.findall('Reviewed-by: .+ <(.+)>',
                                      commit_info['message']))
      commit_details.append(_FormatCommitForBug(
          change.last_commit, commit_info))

    # Header.
    if len(differences) == 1:
      status = 'Found a significant difference after 1 commit.'
    else:
      status = ('Found significant differences after each of %d commits.' %
                len(differences))

    title = '<b>%s %s</b>' % (_ROUND_PUSHPIN, status)
    header = '\n'.join((title, self.url))

    # Body.
    body = '\n\n'.join(commit_details)

    # Footer.
    footer = ('Understanding performance regressions:\n'
              '  http://g.co/ChromePerformanceRegressions')

    # Bring it all together.
    comment = '\n\n'.join((header, body, footer))
    self._PostBugComment(comment, status='Assigned',
                         cc_list=sorted(cc_list), owner=owner)

  def _Fail(self):
    self.exception = traceback.format_exc()

    title = _CRYING_CAT_FACE + ' Pinpoint job stopped with an error.'
    comment = '\n'.join((title, self.url))
    self._PostBugComment(comment)

  def _Schedule(self):
    # Set a task name to deduplicate retries. This adds some latency, but we're
    # not latency-sensitive. If Job.Run() works asynchronously in the future,
    # we don't need to worry about duplicate tasks.
    # https://github.com/catapult-project/catapult/issues/3900
    task_name = str(uuid.uuid4())
    try:
      task = taskqueue.add(
          queue_name='job-queue', url='/api/run/' + self.job_id,
          name=task_name, countdown=_TASK_INTERVAL)
    except apiproxy_errors.DeadlineExceededError:
      task = taskqueue.add(
          queue_name='job-queue', url='/api/run/' + self.job_id,
          name=task_name, countdown=_TASK_INTERVAL)

    self.task = task.name

  def Run(self):
    self.exception = None  # In case the Job succeeds on retry.
    self.task = None  # In case an exception is thrown.

    try:
      if self.auto_explore:
        self.state.Explore()
      work_left = self.state.ScheduleWork()

      # Schedule moar task.
      if work_left:
        self._Schedule()
      else:
        self._Complete()
    except BaseException:
      self._Fail()
      raise
    finally:
      # Don't use `auto_now` for `updated`. When we do data migration, we need
      # to be able to modify the Job without changing the Job's completion time.
      self.updated = datetime.datetime.now()

  def AsDict(self, include_state=True):
    d = {
        'job_id': self.job_id,

        'arguments': self.arguments,
        'auto_explore': self.auto_explore,
        'bug_id': self.bug_id,

        'created': self.created.isoformat(),
        'updated': self.updated.isoformat(),
        'exception': self.exception,
        'status': self.status,
    }
    if include_state:
      d.update(self.state.AsDict())
    return d

  def _PostBugComment(self, *args, **kwargs):
    if not self.bug_id:
      return

    issue_tracker = issue_tracker_service.IssueTrackerService(
        utils.ServiceAccountHttp())
    issue_tracker.AddBugComment(self.bug_id, *args, **kwargs)
Exemple #22
0
class ExampleStore(ndb.Model):
    original_html = ndb.TextProperty('h', indexed=False)
    microdata = ndb.TextProperty('m', indexed=False)
    rdfa = ndb.TextProperty('r', indexed=False)
    jsonld = ndb.TextProperty('j', indexed=False)
    egmeta = ndb.PickleProperty('e', indexed=False)
    keyvalue = ndb.StringProperty('o', indexed=True)
    layer = ndb.StringProperty('l', indexed=False)

    @staticmethod
    def initialise():
        EXAMPLESTORECACHE = []
        import time
        log.info("[%s]ExampleStore initialising Data Store" %
                 (getInstanceId(short=True)))
        loops = 0
        ret = 0
        while loops < 10:
            keys = ExampleStore.query().fetch(keys_only=True,
                                              use_memcache=False,
                                              use_cache=False)
            count = len(keys)
            if count == 0:
                break
            log.info("[%s]ExampleStore deleting %s keys" %
                     (getInstanceId(short=True), count))
            ndb.delete_multi(keys, use_memcache=False, use_cache=False)
            ret += count
            loops += 1
            time.sleep(0.01)
        return {"ExampleStore": ret}

    @staticmethod
    def add(example):
        e = ExampleStore(id=example.keyvalue,
                         original_html=example.original_html,
                         microdata=example.microdata,
                         rdfa=example.rdfa,
                         jsonld=example.jsonld,
                         egmeta=example.egmeta,
                         keyvalue=example.keyvalue,
                         layer=example.layer)
        EXAMPLESTORECACHE.append(e)

    @staticmethod
    def store(examples):
        for e in examples:
            ExampleStore.add(e)

        if len(EXAMPLESTORECACHE):
            ndb.put_multi(EXAMPLESTORECACHE, use_cache=False)

    def get(self, name):
        if name == 'original_html':
            return self.original_html
        if name == 'microdata':
            return self.microdata
        if name == 'rdfa':
            return self.rdfa
        if name == 'jsonld':
            return self.jsonld
        return ""

    @staticmethod
    def getEgmeta(id):
        em = ExampleStore.get_by_id(id)
        ret = em.emeta
        if ret:
            return ret
        return {}
Exemple #23
0
class Session(ndb.Model):
    session_id = ndb.StringProperty()
    user_id = ndb.StringProperty()
    updated = ndb.DateTimeProperty(auto_now=True)
    data = ndb.PickleProperty(compressed=True, default={})

    @staticmethod
    def _generate_sid():
        return security.generate_random_string(entropy=128)

    @staticmethod
    def _serializer():
        engineauth_config = config.load_config()
        return securecookie.SecureCookieSerializer(
            engineauth_config['secret_key'])

    def hash(self):
        """
        Creates a unique hash from the session.
        This will be used to check for session changes.
        :return: A unique hash for the session
        """
        dataStr = repr(self.data)
        return "{}.{}.{}.{}.{}".format(self.session_id, self.user_id,
                                       str(self.updated), hash(dataStr),
                                       len(dataStr))

    def serialize(self):
        values = self.to_dict(include=['session_id', 'user_id'])
        return self._serializer().serialize('_eauth', values)

    @classmethod
    def deserialize(cls, value):
        return cls._serializer().deserialize('_eauth', value)

    @classmethod
    def get_by_value(cls, value):
        v = cls.deserialize(value)
        if v:
            return cls.get_by_sid(v.get('session_id'))
        return None

    @classmethod
    def get_by_sid(cls, sid):
        return cls.get_by_id(sid)

    @classmethod
    def upgrade_to_user_session(cls, session_id, user_id):
        old_session = cls.get_by_sid(session_id)
        new_session = cls.create(user_id=user_id, data=old_session.data)
        old_session.key.delete()
        return new_session

    @classmethod
    def get_by_user_id(cls, user_id):
        # TODO: make sure that the user doesn't have multiple sessions
        user_id = str(user_id)
        return cls.query(cls.user_id == user_id).get()

    @classmethod
    def create(cls, user_id=None, **kwargs):
        if user_id is None:
            session_id = cls._generate_sid()
        else:
            session_id = user_id = str(user_id)
        session = cls(id=session_id,
                      session_id=session_id,
                      user_id=user_id,
                      **kwargs)
        session.put()
        return session

    @classmethod
    def remove_inactive(cls, days_ago=30, now=None):
        import datetime
        # for testing we want to be able to pass a value for now.
        now = now or datetime.datetime.now()
        dtd = now + datetime.timedelta(-days_ago)
        for s in cls.query(cls.updated < dtd).fetch():
            s.key.delete()
Exemple #24
0
class HeaderEntity(ndb.Model):
    content = ndb.PickleProperty()
Exemple #25
0
class TryJob(internal_only_model.InternalOnlyModel):
    """Stores config and tracking info about a single try job."""
    bot = ndb.StringProperty()
    config = ndb.TextProperty()
    bug_id = ndb.IntegerProperty()
    email = ndb.StringProperty()
    rietveld_issue_id = ndb.IntegerProperty()
    rietveld_patchset_id = ndb.IntegerProperty()
    master_name = ndb.StringProperty(default='ChromiumPerf', indexed=False)
    buildbucket_job_id = ndb.StringProperty()
    internal_only = ndb.BooleanProperty(default=False, indexed=True)

    # Bisect run status (e.g., started, failed).
    status = ndb.StringProperty(
        default='pending',
        choices=[
            'pending',  # Created, but job start has not been confirmed.
            'started',  # Job is confirmed started.
            'failed',  # Job terminated, red build.
            'staled',  # No updates from bots.
            'completed',  # Job terminated, green build.
            'aborted',  # Job terminated with abort (purple, early abort).
        ],
        indexed=True)

    # Number of times this job has been tried.
    run_count = ndb.IntegerProperty(default=0)

    # Last time this job was started.
    last_ran_timestamp = ndb.DateTimeProperty()

    job_type = ndb.StringProperty(default='bisect',
                                  choices=['bisect', 'bisect-fyi', 'perf-try'])

    # job_name attribute is used by try jobs of bisect FYI.
    job_name = ndb.StringProperty(default=None)

    # Results data coming from bisect bots.
    results_data = ndb.JsonProperty(indexed=False)

    log_record_id = ndb.StringProperty(indexed=False)

    # Sets of emails of users who has confirmed this TryJob result is bad.
    bad_result_emails = ndb.PickleProperty()

    def SetStarted(self):
        self.status = 'started'
        self.run_count += 1
        self.last_ran_timestamp = datetime.datetime.now()
        self.put()
        if self.bug_id:
            bug_data.SetBisectStatus(self.bug_id, 'started')

    def SetFailed(self):
        self.status = 'failed'
        self.put()
        if self.bug_id:
            bug_data.SetBisectStatus(self.bug_id, 'failed')
        bisect_stats.UpdateBisectStats(self.bot, 'failed')

    def SetStaled(self):
        self.status = 'staled'
        self.put()
        # TODO(chrisphan): Add 'staled' state to bug_data and bisect_stats.
        if self.bug_id:
            bug_data.SetBisectStatus(self.bug_id, 'failed')
        bisect_stats.UpdateBisectStats(self.bot, 'failed')

    def SetCompleted(self):
        self.status = 'completed'
        self.put()
        if self.bug_id:
            bug_data.SetBisectStatus(self.bug_id, 'completed')
        bisect_stats.UpdateBisectStats(self.bot, 'completed')

    def GetConfigDict(self):
        return json.loads(self.config.split('=', 1)[1])

    def CheckFailureFromBuildBucket(self):
        # Buildbucket job id is not always set.
        if not self.buildbucket_job_id:
            return
        job_info = buildbucket_service.GetJobStatus(self.buildbucket_job_id)
        data = job_info.get('build', {})
        # Proceed if the job failed and the job status has
        # not been updated
        if data.get('result') != 'FAILURE' or self.status == 'failed':
            return
        data['result_details'] = json.loads(data['result_details_json'])
        job_updates = {
            'status': 'failed',
            'failure_reason': data.get('failure_reason'),
            'buildbot_log_url': data.get('url')
        }
        details = data.get('result_details')
        if details:
            properties = details.get('properties')
            if properties:
                job_updates['bisect_bot'] = properties.get('buildername')
                job_updates['extra_result_code'] = properties.get(
                    'extra_result_code')
                bisect_config = properties.get('bisect_config')
                if bisect_config:
                    job_updates['try_job_id'] = bisect_config.get('try_job_id')
                    job_updates['bug_id'] = bisect_config.get('bug_id')
                    job_updates['command'] = bisect_config.get('command')
                    job_updates['test_type'] = bisect_config.get('test_type')
                    job_updates['metric'] = bisect_config.get('metric')
                    job_updates['good_revision'] = bisect_config.get(
                        'good_revision')
                    job_updates['bad_revision'] = bisect_config.get(
                        'bad_revision')
        if not self.results_data:
            self.results_data = {}
        self.results_data.update(job_updates)
        self.status = 'failed'
        self.last_ran_timestamp = datetime.datetime.fromtimestamp(
            float(data['updated_ts']) / 1000000)
        self.put()
Exemple #26
0
class Job(ndb.Model):
  """A Pinpoint job."""

  state = ndb.PickleProperty(required=True, compressed=True)

  #####
  # Job arguments passed in through the API.
  #####

  # Request parameters.
  arguments = ndb.JsonProperty(required=True)

  # TODO: The bug id is only used for posting bug comments when a job starts and
  # completes. This probably should not be the responsibility of Pinpoint.
  bug_id = ndb.IntegerProperty()

  comparison_mode = ndb.StringProperty()

  # The Gerrit server url and change id of the code review to update upon
  # completion.
  gerrit_server = ndb.StringProperty()
  gerrit_change_id = ndb.StringProperty()

  # User-provided name of the job.
  name = ndb.StringProperty()

  tags = ndb.JsonProperty()

  # Email of the job creator.
  user = ndb.StringProperty()

  #####
  # Job state generated by running the job.
  #####

  created = ndb.DateTimeProperty(required=True, auto_now_add=True)
  # Don't use `auto_now` for `updated`. When we do data migration, we need
  # to be able to modify the Job without changing the Job's completion time.
  updated = ndb.DateTimeProperty(required=True, auto_now_add=True)

  completed = ndb.ComputedProperty(lambda self: not self.task)
  failed = ndb.ComputedProperty(lambda self: bool(self.exception))

  # The name of the Task Queue task this job is running on. If it's present, the
  # job is running. The task is also None for Task Queue retries.
  task = ndb.StringProperty()

  # The string contents of any Exception that was thrown to the top level.
  # If it's present, the job failed.
  exception = ndb.TextProperty()

  difference_count = ndb.IntegerProperty()

  retry_count = ndb.IntegerProperty(default=0)


  @classmethod
  def New(cls, quests, changes, arguments=None, bug_id=None,
          comparison_mode=None, comparison_magnitude=None, gerrit_server=None,
          gerrit_change_id=None, name=None, pin=None, tags=None, user=None):
    """Creates a new Job, adds Changes to it, and puts it in the Datstore.

    Args:
      quests: An iterable of Quests for the Job to run.
      changes: An iterable of the initial Changes to run on.
      arguments: A dict with the original arguments used to start the Job.
      bug_id: A monorail issue id number to post Job updates to.
      comparison_mode: Either 'functional' or 'performance', which the Job uses
          to figure out whether to perform a functional or performance bisect.
          If None, the Job will not automatically add any Attempts or Changes.
      comparison_magnitude: The estimated size of the regression or improvement
          to look for. Smaller magnitudes require more repeats.
      gerrit_server: Server of the Gerrit code review to update with job
          results.
      gerrit_change_id: Change id of the Gerrit code review to update with job
          results.
      name: The user-provided name of the Job.
      pin: A Change (Commits + Patch) to apply to every Change in this Job.
      tags: A dict of key-value pairs used to filter the Jobs listings.
      user: The email of the Job creator.

    Returns:
      A Job object.
    """
    state = job_state.JobState(
        quests, comparison_mode=comparison_mode,
        comparison_magnitude=comparison_magnitude, pin=pin)
    job = cls(state=state, arguments=arguments or {}, bug_id=bug_id,
              comparison_mode=comparison_mode, gerrit_server=gerrit_server,
              gerrit_change_id=gerrit_change_id,
              name=name, tags=tags, user=user)

    for c in changes:
      job.AddChange(c)

    job.put()
    return job

  @property
  def job_id(self):
    return '%x' % self.key.id()

  @property
  def status(self):
    if not self.completed:
      return 'Running'

    if self.failed:
      return 'Failed'

    return 'Completed'

  @property
  def url(self):
    host = os.environ['HTTP_HOST']
    # TODO(crbug.com/939723): Remove this workaround when not needed.
    if host == 'pinpoint.chromeperf.appspot.com':
      host = 'pinpoint-dot-chromeperf.appspot.com'
    return 'https://%s/job/%s' % (host, self.job_id)

  @property
  def results_url(self):
    if not self.task:
      url = results2.GetCachedResults2(self)
      if url:
        return url
    # Point to the default status page if no results are available.
    return '/results2/%s' % self.job_id

  @property
  def auto_name(self):
    if self.name:
      return self.name

    if self.comparison_mode == job_state.FUNCTIONAL:
      name = 'Functional bisect'
    elif self.comparison_mode == job_state.PERFORMANCE:
      name = 'Performance bisect'
    else:
      name = 'Try job'

    if 'configuration' in self.arguments:
      name += ' on ' + self.arguments['configuration']
      if 'benchmark' in self.arguments:
        name += '/' + self.arguments['benchmark']

    return name

  def AddChange(self, change):
    self.state.AddChange(change)

  def Start(self):
    """Starts the Job and updates it in the Datastore.

    This method is designed to return fast, so that Job creation is responsive
    to the user. It schedules the Job on the task queue without running
    anything. It also posts a bug comment, and updates the Datastore.
    """
    self._Schedule()
    self.put()

    title = _ROUND_PUSHPIN + ' Pinpoint job started.'
    comment = '\n'.join((title, self.url))
    self._PostBugComment(comment, send_email=False)

  def _Complete(self):
    if self.comparison_mode:
      self.difference_count = len(self.state.Differences())

    try:
      results2.ScheduleResults2Generation(self)
    except taskqueue.Error:
      pass

    self._FormatAndPostBugCommentOnComplete()
    self._UpdateGerritIfNeeded()

  def _FormatAndPostBugCommentOnComplete(self):
    if not self.comparison_mode:
      # There is no comparison metric.
      title = "<b>%s Job complete. See results below.</b>" % _ROUND_PUSHPIN
      self._PostBugComment('\n'.join((title, self.url)))
      return

    # There is a comparison metric.
    differences = self.state.Differences()

    if not differences:
      title = "<b>%s Couldn't reproduce a difference.</b>" % _ROUND_PUSHPIN
      self._PostBugComment('\n'.join((title, self.url)))
      return

    # Include list of Changes.
    owner = None
    sheriff = None
    cc_list = set()
    difference_details = []
    for change_a, change_b in differences:
      if change_b.patch:
        commit_info = change_b.patch.AsDict()
      else:
        commit_info = change_b.last_commit.AsDict()

      # TODO: Assign the largest difference, not the last one.
      owner = commit_info['author']
      sheriff = utils.GetSheriffForAutorollCommit(
          commit_info['author'], commit_info['message'])
      cc_list.add(commit_info['author'])

      values_a = self.state.ResultValues(change_a)
      values_b = self.state.ResultValues(change_b)
      difference = _FormatDifferenceForBug(commit_info, values_a, values_b,
                                           self.state.metric)
      difference_details.append(difference)

    # Header.
    if len(differences) == 1:
      status = 'Found a significant difference after 1 commit.'
    else:
      status = ('Found significant differences after each of %d commits.' %
                len(differences))

    title = '<b>%s %s</b>' % (_ROUND_PUSHPIN, status)
    header = '\n'.join((title, self.url))

    # Body.
    body = '\n\n'.join(difference_details)
    if sheriff:
      owner = sheriff
      body += '\n\nAssigning to sheriff %s because "%s" is a roll.' % (
          sheriff, commit_info['subject'])

    # Footer.
    footer = ('Understanding performance regressions:\n'
              '  http://g.co/ChromePerformanceRegressions')

    if differences:
      footer += self._FormatDocumentationUrls()

    # Bring it all together.
    comment = '\n\n'.join((header, body, footer))
    current_bug_status = self._GetBugStatus()
    if (not current_bug_status or
        current_bug_status in ['Untriaged', 'Unconfirmed', 'Available']):
      # Set the bug status and owner if this bug is opened and unowned.
      self._PostBugComment(comment, status='Assigned',
                           cc_list=sorted(cc_list), owner=owner)
    else:
      # Only update the comment and cc list if this bug is assigned or closed.
      self._PostBugComment(comment, cc_list=sorted(cc_list))

  def _FormatDocumentationUrls(self):
    if not self.tags:
      return ''

    # TODO(simonhatch): Tags isn't the best way to get at this, but wait until
    # we move this back into the dashboard so we have a better way of getting
    # at the test path.
    # crbug.com/876899
    test_path = self.tags.get('test_path')
    if not test_path:
      return ''

    test_suite = utils.TestKey('/'.join(test_path.split('/')[:3]))

    docs = histogram.SparseDiagnostic.GetMostRecentDataByNamesSync(
        test_suite, [reserved_infos.DOCUMENTATION_URLS.name])

    if not docs:
      return ''

    docs = docs[reserved_infos.DOCUMENTATION_URLS.name].get('values')

    footer = '\n\n%s:\n  %s' % (docs[0][0], docs[0][1])

    return footer

  def _UpdateGerritIfNeeded(self):
    if self.gerrit_server and self.gerrit_change_id:
      gerrit_service.PostChangeComment(
          self.gerrit_server,
          self.gerrit_change_id,
          '%s Job complete.\n\nSee results at: %s' % (_ROUND_PUSHPIN, self.url))

  def Fail(self, exception=None):
    if exception:
      self.exception = exception
    else:
      self.exception = traceback.format_exc()

    title = _CRYING_CAT_FACE + ' Pinpoint job stopped with an error.'
    exc_info = sys.exc_info()
    exc_message = ''
    if exc_info[1]:
      exc_message = sys.exc_info()[1].message
    elif self.exception:
      exc_message = self.exception.splitlines()[-1]

    comment = '\n'.join((title, self.url, '', exc_message))

    self.task = None

    self._PostBugComment(comment)

  def _Schedule(self, countdown=_TASK_INTERVAL):
    # Set a task name to deduplicate retries. This adds some latency, but we're
    # not latency-sensitive. If Job.Run() works asynchronously in the future,
    # we don't need to worry about duplicate tasks.
    # https://github.com/catapult-project/catapult/issues/3900
    task_name = str(uuid.uuid4())
    try:
      task = taskqueue.add(
          queue_name='job-queue', url='/api/run/' + self.job_id,
          name=task_name, countdown=countdown)
    except (apiproxy_errors.DeadlineExceededError, taskqueue.TransientError):
      task = taskqueue.add(
          queue_name='job-queue', url='/api/run/' + self.job_id,
          name=task_name, countdown=countdown)

    self.task = task.name

  def _MaybeScheduleRetry(self):
    if not hasattr(self, 'retry_count') or self.retry_count is None:
      self.retry_count = 0

    if self.retry_count >= _MAX_RECOVERABLE_RETRIES:
      return False

    self.retry_count += 1

    # Back off exponentially
    self._Schedule(countdown=_TASK_INTERVAL * (2 ** self.retry_count))

    return True

  def Run(self):
    """Runs this Job.

    Loops through all Attempts and checks the status of each one, kicking off
    tasks as needed. Does not block to wait for all tasks to finish. Also
    compares adjacent Changes' results and adds any additional Attempts or
    Changes as needed. If there are any incomplete tasks, schedules another
    Run() call on the task queue.
    """
    self.exception = None  # In case the Job succeeds on retry.
    self.task = None  # In case an exception is thrown.

    try:
      if self.comparison_mode:
        self.state.Explore()
      work_left = self.state.ScheduleWork()

      # Schedule moar task.
      if work_left:
        self._Schedule()
      else:
        self._Complete()

      self.retry_count = 0
    except job_state.JobStateRecoverableError:
      if not self._MaybeScheduleRetry():
        self.Fail()
        raise
    except BaseException:
      self.Fail()
      raise
    finally:
      # Don't use `auto_now` for `updated`. When we do data migration, we need
      # to be able to modify the Job without changing the Job's completion time.
      self.updated = datetime.datetime.now()
      try:
        self.put()
      except (datastore_errors.Timeout,
              datastore_errors.TransactionFailedError):
        # Retry once.
        self.put()
      except datastore_errors.BadRequestError:
        if self.task:
          queue = taskqueue.Queue('job-queue')
          queue.delete_tasks(taskqueue.Task(name=self.task))
        self.task = None

        # The _JobState is too large to fit in an ndb property.
        # Load the Job from before we updated it, and fail it.
        job = self.key.get(use_cache=False)
        job.task = None
        job.Fail()
        job.updated = datetime.datetime.now()
        job.put()
        raise

  def AsDict(self, options=None):
    d = {
        'job_id': self.job_id,
        'results_url': self.results_url,

        'arguments': self.arguments,
        'bug_id': self.bug_id,
        'comparison_mode': self.comparison_mode,
        'name': self.auto_name,
        'user': self.user,

        'created': self.created.isoformat(),
        'updated': self.updated.isoformat(),
        'difference_count': self.difference_count,
        'exception': self.exception,
        'status': self.status,
    }
    if not options:
      return d

    if OPTION_STATE in options:
      d.update(self.state.AsDict())
    if OPTION_TAGS in options:
      d['tags'] = {'tags': self.tags}
    return d

  def _PostBugComment(self, *args, **kwargs):
    if not self.bug_id:
      return

    issue_tracker = issue_tracker_service.IssueTrackerService(
        utils.ServiceAccountHttp())
    issue_tracker.AddBugComment(self.bug_id, *args, **kwargs)

  def _GetBugStatus(self):
    if not self.bug_id:
      return None

    issue_tracker = issue_tracker_service.IssueTrackerService(
        utils.ServiceAccountHttp())
    issue_data = issue_tracker.GetIssue(self.bug_id)
    return issue_data.get('status')
Exemple #27
0
class Issue(ndb.Model):
    number = ndb.IntegerProperty(required=True)
    updated_at = ndb.DateTimeProperty()
    user = ndb.StringProperty()
    state = ndb.StringProperty()
    title = ndb.StringProperty()
    comments_json = ndb.JsonProperty(compressed=True)
    comments_etag = ndb.StringProperty()
    pr_comments_json = ndb.JsonProperty(compressed=True)
    pr_comments_etag = ndb.StringProperty()
    files_json = ndb.JsonProperty(compressed=True)
    files_etag = ndb.StringProperty()
    pr_json = ndb.JsonProperty()
    etag = ndb.StringProperty()
    # Cached properties, while we migrate away from on-the-fly computed ones:
    cached_commenters = ndb.PickleProperty()
    cached_last_jenkins_outcome = ndb.StringProperty()
    last_jenkins_comment = ndb.JsonProperty()

    ASKED_TO_CLOSE_REGEX = re.compile(
        r"""
        (mind\s+closing\s+(this|it))|
        (close\s+this\s+(issue|pr))
    """, re.I | re.X)

    _components = [
        # (name, pr_title_regex, filename_regex)
        ("Core", "core", "^core/"),
        ("Scheduler", "schedul", "scheduler"),
        ("Python", "python|pyspark", "python"),
        ("YARN", "yarn", "yarn"),
        ("Mesos", "mesos", "mesos"),
        ("Web UI", "webui|(web ui)", "spark/ui/"),
        ("Build", "build", "(pom\.xml)|project"),
        ("Docs", "docs", "docs|README"),
        ("EC2", "ec2", "ec2"),
        ("SQL", "sql", "sql"),
        ("MLlib", "mllib", "mllib"),
        ("GraphX", "graphx|pregel", "graphx"),
        ("Streaming", "stream|flume|kafka|twitter|zeromq", "streaming"),
    ]

    @property
    def components(self):
        """
        Returns the list of components used to classify this pull request.

        Components are identified automatically based on the files that the pull request
        modified and any tags added to the pull request's title (such as [GraphX]).
        """
        components = []
        title = ((self.pr_json and self.pr_json["title"]) or self.title or "")
        modified_files = [f["filename"] for f in (self.files_json or [])]
        for (component_name, pr_title_regex,
             filename_regex) in Issue._components:
            if re.search(pr_title_regex, title, re.IGNORECASE) or \
                    any(re.search(filename_regex, f, re.I) for f in modified_files):
                components.append(component_name)
        return components or ["Core"]

    @property
    def parsed_title(self):
        """
        Get a parsed version of this PR's title, which identifies referenced JIRAs and metadata.
        For example, given a PR titled
            "[SPARK-975] [core] Visual debugger of stages and callstacks""
        this will return
            {'jiras': [975], 'title': 'Visual debugger of stages and callstacks', 'metadata': ''}
        """
        return parse_pr_title((self.pr_json and self.pr_json["title"])
                              or self.title or "")

    @property
    def lines_added(self):
        if self.pr_json:
            return self.pr_json.get("additions")
        else:
            return ""

    @property
    def lines_deleted(self):
        if self.pr_json:
            return self.pr_json.get("deletions")
        else:
            return ""

    @property
    def lines_changed(self):
        if self.lines_added != "":
            return self.lines_added + self.lines_deleted
        else:
            return 0

    @property
    def is_mergeable(self):
        return self.pr_json and self.pr_json["mergeable"]

    @property
    def commenters(self):
        if self.cached_commenters is None:
            self.cached_commenters = self._compute_commenters()
            self.put()
        return self.cached_commenters

    @property
    def last_jenkins_outcome(self):
        if self.cached_last_jenkins_outcome is None:
            (outcome, comment) = self._compute_last_jenkins_outcome()
            self.cached_last_jenkins_outcome = outcome
            self.last_jenkins_comment = comment
            self.put()
        return self.cached_last_jenkins_outcome

    def _compute_commenters(self):
        res = defaultdict(
            dict)  # Indexed by user, since we only display each user once.
        excluded_users = set(("SparkQA", "AmplabJenkins"))
        all_comments = sorted(
            (self.comments_json or []) + (self.pr_comments_json or []),
            key=lambda c: c['created_at'])
        for comment in all_comments:
            if is_jenkins_command(comment['body']):
                continue  # Skip comments that solely consist of Jenkins commands
            user = comment['user']['login']
            if user not in excluded_users:
                user_dict = res[user]
                user_dict['url'] = comment['html_url']
                user_dict['avatar'] = comment['user']['avatar_url']
                user_dict['date'] = comment['created_at'],
                user_dict['body'] = comment['body']
                # Display at most 10 lines of context for comments left on diffs:
                user_dict['diff_hunk'] = '\n'.join(
                    comment.get('diff_hunk', '').split('\n')[-10:])
                user_dict['said_lgtm'] = (user_dict.get('said_lgtm')
                                          or re.search("lgtm", comment['body'],
                                                       re.I) is not None)
                user_dict['asked_to_close'] = \
                    (user_dict.get('asked_to_close')
                     or Issue.ASKED_TO_CLOSE_REGEX.search(comment['body']) is not None)
        return sorted(res.items(), key=lambda x: x[1]['date'], reverse=True)

    def _compute_last_jenkins_outcome(self):
        status = "Unknown"
        jenkins_comment = None
        for comment in (self.comments_json or []):
            if contains_jenkins_command(comment['body']):
                status = "Asked"
                jenkins_comment = comment
            elif comment['user']['login'] in ("SparkQA", "AmplabJenkins"):
                body = comment['body'].lower()
                jenkins_comment = comment
                if "pass" in body:
                    status = "Pass"
                elif "fail" in body:
                    status = "Fail"
                elif "started" in body:
                    status = "Running"
                elif "can one of the admins verify this patch?" in body:
                    status = "Verify"
                elif "timed out" in body:
                    status = "Timeout"
                else:
                    status = "Unknown"  # So we display "Unknown" instead of an out-of-date status
        return (status, jenkins_comment)

    @classmethod
    def get_or_create(cls, number):
        key = str(ndb.Key("Issue", number).id())
        return Issue.get_or_insert(key, number=number)

    def update(self, oauth_token):
        logging.debug("Updating pull request %i" % self.number)
        # Record basic information about this pull request
        issue_response = raw_github_request(PULLS_BASE + '/%i' % self.number,
                                            oauth_token=oauth_token,
                                            etag=self.etag)
        if issue_response is None:
            logging.debug("PR %i hasn't changed since last visit; skipping" %
                          self.number)
            return
        self.pr_json = json.loads(issue_response.content)
        self.etag = issue_response.headers["ETag"]
        updated_at = \
            parse_datetime(self.pr_json['updated_at']).astimezone(tz.tzutc()).replace(tzinfo=None)
        self.user = self.pr_json['user']['login']
        self.updated_at = updated_at
        self.state = self.pr_json['state']

        comments_response = paginated_github_request(
            ISSUES_BASE + '/%i/comments' % self.number,
            oauth_token=oauth_token,
            etag=self.comments_etag)
        if comments_response is not None:
            self.comments_json, self.comments_etag = comments_response

        pr_comments_response = paginated_github_request(
            PULLS_BASE + '/%i/comments' % self.number,
            oauth_token=oauth_token,
            etag=self.pr_comments_etag)
        if pr_comments_response is not None:
            self.pr_comments_json, self.pr_comments_etag = pr_comments_response

        files_response = paginated_github_request(PULLS_BASE +
                                                  "/%i/files" % self.number,
                                                  oauth_token=oauth_token,
                                                  etag=self.files_etag)
        if files_response is not None:
            self.files_json, self.files_etag = files_response

        self.cached_last_jenkins_outcome = None
        self.last_jenkins_outcome  # force recomputation of Jenkins outcome
        self.cached_commenters = self._compute_commenters()

        for issue_number in self.parsed_title['jiras']:
            try:
                link_issue_to_pr("SPARK-%s" % issue_number, self)
            except:
                logging.exception(
                    "Exception when linking to JIRA issue SPARK-%s" %
                    issue_number)

        self.put()  # Write our modifications back to the database
Exemple #28
0
class Drone(ndb.Model):
    """Models an individual drone."""
    drone_name = ndb.StringProperty(indexed=True)
    range_in_kilometers = ndb.FloatProperty()
    update_time = ndb.DateTimeProperty()
    location_cost_map = ndb.PickleProperty()
Exemple #29
0
class Job(ndb.Model):
    """A Pinpoint job."""

    created = ndb.DateTimeProperty(required=True, auto_now_add=True)
    # Don't use `auto_now` for `updated`. When we do data migration, we need
    # to be able to modify the Job without changing the Job's completion time.
    updated = ndb.DateTimeProperty(required=True, auto_now_add=True)

    # The name of the Task Queue task this job is running on. If it's present, the
    # job is running. The task is also None for Task Queue retries.
    task = ndb.StringProperty()

    # The string contents of any Exception that was thrown to the top level.
    # If it's present, the job failed.
    exception = ndb.TextProperty()

    # Request parameters.
    arguments = ndb.JsonProperty(required=True)

    # If True, the service should pick additional Changes to run (bisect).
    # If False, only run the Changes explicitly added by the user.
    auto_explore = ndb.BooleanProperty(required=True)

    # The metric to use when determining whether to add additional Attempts or
    # Changes to the Job. If None, the Job will use a fixed number of Attempts.
    comparison_mode = msgprop.EnumProperty(ComparisonMode)

    # TODO: The bug id is only used for posting bug comments when a job starts and
    # completes. This probably should not be the responsibility of Pinpoint.
    bug_id = ndb.IntegerProperty()

    # Email of the job creator.
    user = ndb.StringProperty()

    state = ndb.PickleProperty(required=True, compressed=True)

    tags = ndb.JsonProperty()

    @classmethod
    def New(cls,
            arguments,
            quests,
            auto_explore,
            comparison_mode=None,
            user=None,
            bug_id=None,
            tags=None):
        # Create job.
        return cls(arguments=arguments,
                   auto_explore=auto_explore,
                   comparison_mode=comparison_mode,
                   user=user,
                   bug_id=bug_id,
                   tags=tags,
                   state=job_state.JobState(quests))

    @property
    def job_id(self):
        return '%x' % self.key.id()

    @property
    def status(self):
        if self.task:
            return 'Running'

        if self.exception:
            return 'Failed'

        return 'Completed'

    @property
    def url(self):
        return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id)

    def AddChange(self, change):
        self.state.AddChange(change)

    def Start(self):
        self._Schedule()

        title = _ROUND_PUSHPIN + ' Pinpoint job started.'
        comment = '\n'.join((title, self.url))
        self._PostBugComment(comment, send_email=False)

    def _Complete(self):
        try:
            results2.ScheduleResults2Generation(self)
        except taskqueue.Error:
            pass

        # Format bug comment.

        if not self.auto_explore:
            # There is no comparison metric.
            title = "<b>%s Job complete. See results below.</b>" % _ROUND_PUSHPIN
            self._PostBugComment('\n'.join((title, self.url)))
            return

        # There is a comparison metric.
        differences = tuple(self.state.Differences())

        if not differences:
            title = "<b>%s Couldn't reproduce a difference.</b>" % _ROUND_PUSHPIN
            self._PostBugComment('\n'.join((title, self.url)))
            return

        # Include list of Changes.
        owner = None
        cc_list = set()
        commit_details = []
        for _, change in differences:
            if change.patch:
                commit_info = change.patch.AsDict()
            else:
                commit_info = change.last_commit.AsDict()

            # TODO: Assign the largest difference, not the last one.
            owner = commit_info['author']
            cc_list.add(commit_info['author'])
            if 'reviewers' in commit_info:
                cc_list |= frozenset(commit_info['reviewers'])
            commit_details.append(_FormatCommitForBug(commit_info))

        # Header.
        if len(differences) == 1:
            status = 'Found a significant difference after 1 commit.'
        else:
            status = (
                'Found significant differences after each of %d commits.' %
                len(differences))

        title = '<b>%s %s</b>' % (_ROUND_PUSHPIN, status)
        header = '\n'.join((title, self.url))

        # Body.
        body = '\n\n'.join(commit_details)

        # Footer.
        footer = ('Understanding performance regressions:\n'
                  '  http://g.co/ChromePerformanceRegressions')

        # Bring it all together.
        comment = '\n\n'.join((header, body, footer))
        current_bug_status = self._GetBugStatus()
        if (not current_bug_status or current_bug_status
                in ['Untriaged', 'Unconfirmed', 'Available']):
            # Set the bug status and owner if this bug is opened and unowned.
            self._PostBugComment(comment,
                                 status='Assigned',
                                 cc_list=sorted(cc_list),
                                 owner=owner)
        else:
            # Only update the comment and cc list if this bug is assigned or closed.
            self._PostBugComment(comment, cc_list=sorted(cc_list))

    def Fail(self):
        self.exception = traceback.format_exc()

        title = _CRYING_CAT_FACE + ' Pinpoint job stopped with an error.'
        comment = '\n'.join((title, self.url, '', sys.exc_value.message))
        self._PostBugComment(comment)

    def _Schedule(self):
        # Set a task name to deduplicate retries. This adds some latency, but we're
        # not latency-sensitive. If Job.Run() works asynchronously in the future,
        # we don't need to worry about duplicate tasks.
        # https://github.com/catapult-project/catapult/issues/3900
        task_name = str(uuid.uuid4())
        try:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=_TASK_INTERVAL)
        except apiproxy_errors.DeadlineExceededError:
            task = taskqueue.add(queue_name='job-queue',
                                 url='/api/run/' + self.job_id,
                                 name=task_name,
                                 countdown=_TASK_INTERVAL)

        self.task = task.name

    def Run(self):
        self.exception = None  # In case the Job succeeds on retry.
        self.task = None  # In case an exception is thrown.

        try:
            if self.auto_explore:
                self.state.Explore()
            work_left = self.state.ScheduleWork()

            # Schedule moar task.
            if work_left:
                self._Schedule()
            else:
                self._Complete()
        except BaseException:
            self.Fail()
            raise
        finally:
            # Don't use `auto_now` for `updated`. When we do data migration, we need
            # to be able to modify the Job without changing the Job's completion time.
            self.updated = datetime.datetime.now()
            try:
                self.put()
            except (datastore_errors.Timeout,
                    datastore_errors.TransactionFailedError):
                # Retry once.
                self.put()
            except datastore_errors.BadRequestError:
                if self.task:
                    queue = taskqueue.Queue('job-queue')
                    queue.delete_tasks(taskqueue.Task(name=self.task))
                self.task = None

                # The _JobState is too large to fit in an ndb property.
                # Load the Job from before we updated it, and fail it.
                job = self.key.get(use_cache=False)
                job.task = None
                job.Fail()
                job.updated = datetime.datetime.now()
                job.put()
                raise

    def AsDict(self, options=None):
        d = {
            'job_id': self.job_id,
            'arguments': self.arguments,
            'auto_explore': self.auto_explore,
            'bug_id': self.bug_id,
            'user': self.user,
            'created': self.created.isoformat(),
            'updated': self.updated.isoformat(),
            'exception': self.exception,
            'status': self.status,
        }
        if not options:
            return d

        if OPTION_STATE in options:
            d.update(self.state.AsDict())
        if OPTION_TAGS in options:
            d['tags'] = {'tags': self.tags}
        return d

    def _PostBugComment(self, *args, **kwargs):
        if not self.bug_id:
            return

        issue_tracker = issue_tracker_service.IssueTrackerService(
            utils.ServiceAccountHttp())
        issue_tracker.AddBugComment(self.bug_id, *args, **kwargs)

    def _GetBugStatus(self):
        if not self.bug_id:
            return None

        issue_tracker = issue_tracker_service.IssueTrackerService(
            utils.ServiceAccountHttp())
        issue_data = issue_tracker.GetIssue(self.bug_id)
        return issue_data.get('status')
Exemple #30
0
class TryJob(internal_only_model.InternalOnlyModel):
  """Stores config and tracking info about a single try job."""
  bot = ndb.StringProperty()
  config = ndb.TextProperty()
  bug_id = ndb.IntegerProperty()
  email = ndb.StringProperty()
  rietveld_issue_id = ndb.IntegerProperty()
  rietveld_patchset_id = ndb.IntegerProperty()
  master_name = ndb.StringProperty(default='ChromiumPerf', indexed=False)
  buildbucket_job_id = ndb.StringProperty()
  internal_only = ndb.BooleanProperty(default=False, indexed=True)

  # Bisect run status (e.g., started, failed).
  status = ndb.StringProperty(
      default='pending',
      choices=[
          'pending',  # Created, but job start has not been confirmed.
          'started',  # Job is confirmed started.
          'failed',   # Job terminated, red build.
          'staled',   # No updates from bots.
          'completed',  # Job terminated, green build.
          'aborted',  # Job terminated with abort (purple, early abort).
      ],
      indexed=True)

  # Last time this job was started.
  last_ran_timestamp = ndb.DateTimeProperty()

  job_type = ndb.StringProperty(
      default='bisect',
      choices=['bisect', 'bisect-fyi', 'perf-try'])

  # job_name attribute is used by try jobs of bisect FYI.
  job_name = ndb.StringProperty(default=None)

  # Results data coming from bisect bots.
  results_data = ndb.JsonProperty(indexed=False)

  log_record_id = ndb.StringProperty(indexed=False)

  # Sets of emails of users who has confirmed this TryJob result is bad.
  bad_result_emails = ndb.PickleProperty()

  def SetStarted(self):
    self.status = 'started'
    self.last_ran_timestamp = datetime.datetime.now()
    self.put()
    if self.bug_id:
      bug_data.SetBisectStatus(self.bug_id, 'started')

  def SetFailed(self):
    self.status = 'failed'
    self.put()
    if self.bug_id:
      bug_data.SetBisectStatus(self.bug_id, 'failed')
    bisect_stats.UpdateBisectStats(self.bot, 'failed')

  def SetStaled(self):
    self.status = 'staled'
    self.put()
    logging.info('Updated status to staled')
    # TODO(sullivan, dtu): what is the purpose of 'staled' status? Doesn't it
    # just prevent updating jobs older than 24 hours???
    # TODO(chrisphan): Add 'staled' state to bug_data and bisect_stats.
    if self.bug_id:
      bug_data.SetBisectStatus(self.bug_id, 'failed')
    bisect_stats.UpdateBisectStats(self.bot, 'failed')

  def SetCompleted(self):
    logging.info('Updated status to completed')
    self.status = 'completed'
    self.put()
    if self.bug_id:
      bug_data.SetBisectStatus(self.bug_id, 'completed')
    bisect_stats.UpdateBisectStats(self.bot, 'completed')

  def GetCulpritCL(self):
    if not self.results_data:
      return None
    # culprit_data can be undefined or explicitly set to None
    culprit_data = self.results_data.get('culprit_data') or {}
    return culprit_data.get('cl')

  def GetConfigDict(self):
    return json.loads(self.config.split('=', 1)[1])

  def CheckFailureFromBuildBucket(self):
    # Buildbucket job id is not always set.
    if not self.buildbucket_job_id:
      return
    job_info = buildbucket_service.GetJobStatus(self.buildbucket_job_id)
    data = job_info.get('build', {})

    # Since the job is completed successfully, results_data must
    # have been set appropriately by the bisector.
    # The buildbucket job's 'status' and 'result' fields are documented here:
    # https://goto.google.com/bb_status
    if data.get('status') == 'COMPLETED' and data.get('result') == 'SUCCESS':
      return

    # Proceed if the job failed or cancelled
    logging.info('Job failed. Buildbucket id %s', self.buildbucket_job_id)
    data['result_details'] = json.loads(data['result_details_json'])
    # There are various failure and cancellation reasons for a buildbucket
    # job to fail as listed in https://goto.google.com/bb_status.
    job_updates = {
        'failure_reason': (data.get('cancelation_reason') or
                           data.get('failure_reason')),
        'buildbot_log_url': data.get('url')
    }
    details = data.get('result_details')
    if details:
      properties = details.get('properties')
      if properties:
        job_updates['bisect_bot'] = properties.get('buildername')
        job_updates['extra_result_code'] = properties.get(
            'extra_result_code')
        bisect_config = properties.get('bisect_config')
        if bisect_config:
          job_updates['try_job_id'] = bisect_config.get('try_job_id')
          job_updates['bug_id'] = bisect_config.get('bug_id')
          job_updates['command'] = bisect_config.get('command')
          job_updates['test_type'] = bisect_config.get('test_type')
          job_updates['metric'] = bisect_config.get('metric')
          job_updates['good_revision'] = bisect_config.get('good_revision')
          job_updates['bad_revision'] = bisect_config.get('bad_revision')
    if not self.results_data:
      self.results_data = {}
    self.results_data.update(job_updates)
    self.status = 'failed'
    self.last_ran_timestamp = datetime.datetime.fromtimestamp(
        float(data['updated_ts'])/1000000)
    self.put()
    logging.info('updated status to failed.')