Sort issues by their age in the commit queue.

Previously, CQ was processing issues sorted by issue ID, which may result in processing a very recent CL before a very old CL. The age is based on when the Commit box was last checked (and not when the CL was first created).

This will hopefully help to clear the oldest CLs first, improve patch success rate, and keep the CQ's maximum age down.

BUG=335187

Review URL: https://codereview.chromium.org/145293006

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/commit-queue@247767 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/pending_manager.py b/pending_manager.py
index c392a91..6f61d4b 100644
--- a/pending_manager.py
+++ b/pending_manager.py
@@ -13,6 +13,7 @@
   - SVN will check the committer write access.
 """
 
+import datetime
 import errno
 import logging
 import os
@@ -47,6 +48,7 @@
   base_url = unicode
   messages = list
   relpath = unicode
+  sort_key = unicode
   # Only used after a patch was committed. Keeping here for try job retries.
   revision = (None, int, unicode)
 
@@ -117,8 +119,8 @@
     return self.pending_commits[str(key)]
 
   def iterate(self):
-    """Returns the items sorted by issue id to ease testability."""
-    return sorted(self.pending_commits.itervalues(), key=lambda x: x.issue)
+    """Returns the items sorted by .sort_key to ease testability."""
+    return sorted(self.pending_commits.itervalues(), key=lambda x: x.sort_key)
 
   def remove(self, key):
     self.pending_commits.pop(str(key), None)
@@ -174,6 +176,34 @@
     for verifier in self.pre_patch_verifiers:
       assert not isinstance(verifier, base.VerifierCheckout)
 
+  def _get_user(self):
+    """Get the CQ's rietveld user name.
+
+    We need it to look for messages posted by the CQ, to figure out
+    when the commit box was checked.  TODO(sergeyberezin): when
+    Rietveld can tell this info natively, this function will be
+    obsolete.
+    """
+    # Rietveld object or its email may be missing in some unittests.
+    if self.context.rietveld and self.context.rietveld.email:
+      return self.context.rietveld.email
+    else:
+      return 'commit-bot@chromium.org'
+
+  def _set_sort_key(self, issue_data):
+    """Compute the sorting key. Must add .sort_key to issue_data.
+
+    Previously we used issue id, but a better key is the last
+    timestamp when Commit box was checked.
+    """
+    for m in issue_data['messages']:
+      if (m['sender'] == self._get_user() and
+          'CQ is trying da patch.' in m['text']):
+        issue_data['sort_key'] = m['date']
+    if 'sort_key' not in issue_data:
+      # Set a default value: the current time.
+      issue_data['sort_key'] = str(datetime.datetime.utcnow())
+
   def look_for_new_pending_commit(self):
     """Looks for new reviews on self.context.rietveld with c+ set.
 
@@ -206,6 +236,7 @@
         try:
           issue_data = self.context.rietveld.get_issue_properties(
               issue_id, True)
+          self._set_sort_key(issue_data)
         except urllib2.HTTPError as e:
           if e.code in (500, 502, 503):
             # Temporary AppEngine hiccup. Just log it and continue.
@@ -249,7 +280,8 @@
                   patchset=issue_data['patchsets'][-1],
                   base_url=issue_data['base_url'],
                   description=issue_data['description'].replace('\r', ''),
-                  messages=issue_data['messages']))
+                  messages=issue_data['messages'],
+                  sort_key=issue_data['sort_key']))
 
   def process_new_pending_commit(self):
     """Starts verification on newly found pending commits."""
@@ -262,8 +294,8 @@
         if (not missing or pending.get_state() != base.PROCESSING):
           continue
         logging.info(
-            'Processing issue %s (%s, %d)' % (
-                pending.issue, missing, pending.get_state()))
+            'Processing issue %s @ %s (%s, %d)' % (
+                pending.issue, pending.sort_key, missing, pending.get_state()))
         self._verify_pending(pending)
       except base.DiscardPending as e:
         message = e.status