blob: 13a1066693beda15aa38750a7a9b1eedb2033568 [file] [log] [blame]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Commit queue status."""
import cgi
import datetime
import json
import logging
import re
import sys
import urllib2
from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext.db import polymodel
from base_page import BasePage
import utils
TRY_SERVER_MAP = (
'SUCCESS', 'WARNINGS', 'FAILURE', 'SKIPPED', 'EXCEPTION', 'RETRY',
)
# Verification name in commit-queue/verification/*.py. Initialized at the bottom
# of this file.
EVENT_MAP = {}
class Owner(db.Model):
"""key == email address."""
email = db.EmailProperty()
@staticmethod
def to_key(owner):
return '<%s>' % owner
class PendingCommit(db.Model):
"""parent is Owner."""
created = db.DateTimeProperty()
done = db.BooleanProperty(default=False)
issue = db.IntegerProperty()
patchset = db.IntegerProperty()
@staticmethod
def to_key(issue, patchset, owner):
# TODO(maruel): My bad, shouldn't have put owner in the key.
return '<%d-%d-%s>' % (issue, patchset, owner)
class VerificationEvent(polymodel.PolyModel):
"""parent is PendingCommit."""
created = db.DateTimeProperty(auto_now_add=True)
result = db.IntegerProperty()
timestamp = db.DateTimeProperty()
@property
def as_html(self):
raise NotImplementedError()
class TryServerEvent(VerificationEvent):
name = 'try server'
build = db.IntegerProperty()
builder = db.StringProperty()
clobber = db.BooleanProperty()
job_name = db.StringProperty()
revision = db.IntegerProperty()
url = db.StringProperty()
@property
def as_html(self):
if self.build is not None:
out = '<a href="%s">"%s" on %s, build #%s</a>' % (
cgi.escape(self.url),
cgi.escape(self.job_name),
cgi.escape(self.builder),
cgi.escape(str(self.build)))
if (self.result is not None and
0 <= self.result < len(TRY_SERVER_MAP[self.result])):
out = '%s - result: %s' % (out, TRY_SERVER_MAP[self.result])
return out
else:
# TODO(maruel): Load the json
# ('http://build.chromium.org/p/tryserver.chromium/json/builders/%s/'
# 'pendingBuilds') % self.builder and display the rank.
return '"%s" on %s (pending)' % (
cgi.escape(self.job_name),
cgi.escape(self.builder))
@classmethod
def to_key(cls, packet):
if not packet.get('builder') or not packet.get('job_name'):
return None
return '<%s-%s-%s>' % (
cls.name, packet['builder'], packet['job_name'])
class PresubmitEvent(VerificationEvent):
name = 'presubmit'
duration = db.FloatProperty()
output = db.TextProperty()
timed_out = db.BooleanProperty()
@property
def as_html(self):
return '<pre class="output">%s</pre>' % cgi.escape(self.output)
@classmethod
def to_key(cls, _):
# There shall be only one PresubmitEvent per PendingCommit.
return '<%s>' % cls.name
def create_html_links(message):
r = re.compile(r"(\(?)(https?://[^ )]+)(\)?)")
return r.sub(r'\1<a href="\2">\2</a>\3', message)
class CommitEvent(VerificationEvent):
name = 'commit'
output = db.TextProperty()
url = db.StringProperty()
@property
def as_html(self):
return '<pre class="output">%s</pre>' % create_html_links(
cgi.escape(self.output))
@classmethod
def to_key(cls, _):
return '<%s>' % cls.name
class InitialEvent(VerificationEvent):
name = 'initial'
revision = db.IntegerProperty()
@property
def as_html(self):
return 'Looking at new commit, using revision %s' % (
cgi.escape(str(self.revision)))
@classmethod
def to_key(cls, _):
return '<%s>' % cls.name
class AbortEvent(VerificationEvent):
name = 'abort'
output = db.TextProperty()
@property
def as_html(self):
return '<pre class="output">%s</pre>' % create_html_links(
cgi.escape(self.output))
@classmethod
def to_key(cls, _):
return '<%s>' % cls.name
class WhyNotEvent(VerificationEvent):
name = 'why not'
message = db.TextProperty()
@property
def as_html(self):
return '<pre class="output">%s</pre>' % create_html_links(
cgi.escape(self.message))
@classmethod
def to_key(cls, _):
return '<%s>' % cls.name
def get_owner(owner):
"""Efficient querying of Owner with memcache."""
key = Owner.to_key(owner)
obj = memcache.get(key, namespace='Owner')
if not obj:
obj = Owner.get_or_insert(key_name=key, email=owner)
memcache.set(key, obj, time=60*60, namespace='Owner')
return obj
def get_pending_commit(issue, patchset, owner, timestamp):
"""Efficient querying of PendingCommit with memcache."""
owner_obj = get_owner(owner)
key = PendingCommit.to_key(issue, patchset, owner)
obj = memcache.get(key, namespace='PendingCommit')
if not obj:
obj = PendingCommit.get_or_insert(
key_name=key, parent=owner_obj, issue=issue, patchset=patchset,
owner=owner, created=timestamp)
memcache.set(key, obj, time=60*60, namespace='PendingCommit')
return obj
class CQBasePage(BasePage):
"""Returns a web page or json data about commit queue state.
Can filter for everyone, a particular user or a particular issue.
Query args:
- format: can be 'html' or 'json'.
- limit: maximum number of elements to result. default is 100.
"""
def get(self, *args):
query = self._get_query(*args)
if not query:
# The user probably used /me without being logged.
self.redirect(users.create_login_url(self.request.url))
return
out_format = self.request.get('format', 'html')
if out_format == 'json':
return self._get_as_json(query)
else:
return self._get_as_html(query)
def _get_query(self, owner=None, issue=None, patchset=None):
"""Returns None on query failure."""
query = VerificationEvent.all().order('-timestamp')
ancestor = None
if owner:
owner = self._parse_user(owner)
if not owner:
return None
ancestor = db.Key.from_path('Owner', Owner.to_key(owner))
if issue:
issue = int(issue)
if patchset:
patchset = int(patchset)
pending_key = PendingCommit.to_key(issue, patchset, owner)
ancestor = db.Key.from_path(
'PendingCommit', pending_key, parent=ancestor)
else:
# Only show the last object since it's complex to do a OR with multiple
# ancestors.
ancestor = db.Query(PendingCommit, keys_only=True).filter(
'issue =', issue).ancestor(ancestor).order('-created').get()
if ancestor:
query.ancestor(ancestor)
return query
def _get_limit(self):
limit = self.request.get('limit')
if limit and limit.isdigit():
limit = int(limit)
else:
limit = 100
return limit
def _get_as_json(self, query):
self.response.headers['Content-Type'] = 'application/json'
self.response.headers['Access-Control-Allow-Origin'] = '*'
data = json.dumps([s.AsDict() for s in query.fetch(self._get_limit())])
callback = self.request.get('callback')
if callback:
if re.match(r'^[a-zA-Z$_][a-zA-Z$0-9._]*$', callback):
data = '%s(%s);' % (callback, data)
self.response.out.write(data)
def _get_as_html(self, query):
raise NotImplementedError()
def _parse_user(self, user):
user = urllib2.unquote(user.strip('/'))
if user == 'me':
if not self.user:
user = None
else:
user = self.user.email()
return user
class User(CQBasePage):
def _get_as_html(self, query):
pending_commits_events = {}
pending_commits = {}
for event in query.fetch(self._get_limit()):
# Implicitly find PendingCommit's.
pending_commit = event.parent()
if not pending_commit:
logging.warn('Event %s is corrupted, can\'t find %s' % (
event.key().id_or_name(), event.parent_key().id_or_name()))
continue
pending_commits_events.setdefault(pending_commit.key(), []).append(event)
pending_commits[pending_commit.key()] = pending_commit
sorted_data = []
for pending_commit in sorted(
pending_commits.itervalues(), key=lambda x: x.created, reverse=True):
sorted_data.append(
(pending_commit,
reversed(pending_commits_events[pending_commit.key()])))
template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
template_values['data'] = sorted_data
self.DisplayTemplate('cq_owner.html', template_values, use_cache=True)
class Issue(CQBasePage):
def _get_as_html(self, query):
pending_commits_events = {}
pending_commits = {}
for event in query.fetch(self._get_limit()):
# Implicitly find PendingCommit's.
pending_commit = event.parent()
if not pending_commit:
logging.warn('Event %s is corrupted, can\'t find %s' % (
event.key().id_or_name(), event.parent_key().id_or_name()))
continue
pending_commits_events.setdefault(pending_commit.key(), []).append(event)
pending_commits[pending_commit.key()] = pending_commit
sorted_data = []
for pending_commit in sorted(
pending_commits.itervalues(), key=lambda x: x.issue):
sorted_data.append(
(pending_commit,
reversed(pending_commits_events[pending_commit.key()])))
template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
template_values['data'] = sorted_data
self.DisplayTemplate('cq_owner.html', template_values, use_cache=True)
class Receiver(BasePage):
@utils.admin_only
def post(self):
def load_values():
for p in self.request.get_all('p'):
try:
yield json.loads(p)
except ValueError:
logging.warn('Discarding invalid packet %r' % p)
count = 0
for packet in load_values():
cls = EVENT_MAP.get(packet.get('verification'))
if (not cls or
not isinstance(packet.get('issue'), int) or
not isinstance(packet.get('patchset'), int) or
not packet.get('timestamp') or
not isinstance(packet.get('owner'), basestring)):
logging.warning('Ignoring packet %s' % packet)
continue
payload = packet.get('payload', {})
# TODO(maruel): Convert the type implicitly, because storing a int into a
# FloatProperty or a StringProperty will raise a BadValueError.
values = dict(
(i, payload[i]) for i in cls.properties()
if i not in ('_class', 'pending') and i in payload)
# Inject the timestamp.
values['timestamp'] = datetime.datetime.utcfromtimestamp(
packet['timestamp'])
pending = get_pending_commit(
packet['issue'], packet['patchset'], packet['owner'],
values['timestamp'])
logging.debug('New packet %s' % cls.__name__)
key_name = cls.to_key(values)
if not key_name:
continue
# TODO(maruel) Use an async transaction, in batch.
obj = cls.get_by_key_name(key_name, parent=pending)
# Compare the timestamps. Events could arrive in the reverse order.
if not obj or obj.timestamp <= values['timestamp']:
# This will override the previous obj if it existed.
cls(parent=pending, key_name=key_name, **values).put()
count += 1
elif obj:
logging.warn('Received object out of order')
# Cache the fact that the change was committed in the PendingCommit.
if packet['verification'] == 'commit':
pending.done = True
pending.put()
self.response.out.write('%d\n' % count)
def bootstrap():
# Used by _parse_packet() to find the right model to use from the
# 'verification' value of the packet.
module = sys.modules[__name__]
for i in dir(module):
if i.endswith('Event') and i != 'VerificationEvent':
obj = getattr(module, i)
EVENT_MAP[obj.name] = obj