← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-ref-tolerate-slow-commit-info into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-ref-tolerate-slow-commit-info into lp:launchpad.

Commit message:
Tolerate backend timeouts while fetching commit information for GitRef:+index.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1804395 in Launchpad itself: "Timeouts when accessing repo branches"
  https://bugs.launchpad.net/launchpad/+bug/1804395

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-ref-tolerate-slow-commit-info/+merge/359827
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-ref-tolerate-slow-commit-info into lp:launchpad.
=== modified file 'lib/lp/code/browser/gitref.py'
--- lib/lp/code/browser/gitref.py	2018-08-30 14:54:37 +0000
+++ lib/lp/code/browser/gitref.py	2018-11-29 12:18:57 +0000
@@ -49,6 +49,7 @@
 from lp.code.interfaces.gitrepository import IGitRepositorySet
 from lp.services.helpers import english_list
 from lp.services.propertycache import cachedproperty
+from lp.services.scripts import log
 from lp.services.webapp import (
     canonical_url,
     ContextMenu,
@@ -180,7 +181,7 @@
     @cachedproperty
     def commit_infos(self):
         return self.context.getLatestCommits(
-            extended_details=True, user=self.user)
+            extended_details=True, user=self.user, logger=log)
 
     @property
     def recipes_link(self):

=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
--- lib/lp/code/browser/tests/test_gitref.py	2018-09-27 13:50:06 +0000
+++ lib/lp/code/browser/tests/test_gitref.py	2018-11-29 12:18:57 +0000
@@ -11,10 +11,14 @@
 import hashlib
 import re
 
+from fixtures import FakeLogger
 import pytz
 import soupmatchers
 from storm.store import Store
-from testtools.matchers import Equals
+from testtools.matchers import (
+    Equals,
+    Not,
+    )
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -23,6 +27,7 @@
 from lp.code.tests.helpers import GitHostingFixture
 from lp.services.beautifulsoup import BeautifulSoup
 from lp.services.job.runner import JobRunner
+from lp.services.timeout import TimeoutError
 from lp.services.utils import seconds_since_epoch
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
@@ -82,6 +87,14 @@
             canonical_url(ref))
 
 
+class MissingCommitsNote(soupmatchers.Tag):
+
+    def __init__(self):
+        super(MissingCommitsNote, self).__init__(
+            "missing commits note", "div",
+            text="Some recent commit information could not be fetched.")
+
+
 class TestGitRefView(BrowserTestCase):
 
     layer = LaunchpadFunctionalLayer
@@ -186,11 +199,12 @@
         self.hosting_fixture.getLog.result = list(reversed(log))
         self.scanRef(ref, log[-1])
         view = create_initialized_view(ref, "+index")
+        contents = view()
         expected_texts = list(reversed([
             "%.7s...\nby\n%s\non 2015-01-%02d" % (
                 log[i]["sha1"], log[i]["author"]["name"], i + 1)
             for i in range(5)]))
-        details = find_tags_by_class(view(), "commit-details")
+        details = find_tags_by_class(contents, "commit-details")
         self.assertEqual(
             expected_texts, [extract_text(detail) for detail in details])
         expected_urls = list(reversed([
@@ -199,6 +213,8 @@
             for i in range(5)]))
         self.assertEqual(
             expected_urls, [detail.a["href"] for detail in details])
+        self.assertThat(
+            contents, Not(soupmatchers.HTMLContains(MissingCommitsNote())))
 
     def test_recent_commits_with_merge(self):
         [ref] = self.factory.makeGitRefs(paths=["refs/heads/branch"])
@@ -211,7 +227,8 @@
         self.scanRef(mp.merge_source, merged_tip)
         mp.markAsMerged(merged_revision_id=log[0]["sha1"])
         view = create_initialized_view(ref, "+index")
-        soup = BeautifulSoup(view())
+        contents = view()
+        soup = BeautifulSoup(contents)
         details = soup.findAll(
             attrs={"class": re.compile(r"commit-details|commit-comment")})
         expected_texts = list(reversed([
@@ -225,6 +242,8 @@
         self.assertEqual(
             [canonical_url(mp), canonical_url(mp.merge_source)],
             [link["href"] for link in details[5].findAll("a")])
+        self.assertThat(
+            contents, Not(soupmatchers.HTMLContains(MissingCommitsNote())))
 
     def test_recent_commits_with_merge_from_deleted_ref(self):
         [ref] = self.factory.makeGitRefs(paths=["refs/heads/branch"])
@@ -238,7 +257,8 @@
         mp.markAsMerged(merged_revision_id=log[0]["sha1"])
         mp.source_git_repository.removeRefs([mp.source_git_path])
         view = create_initialized_view(ref, "+index")
-        soup = BeautifulSoup(view())
+        contents = view()
+        soup = BeautifulSoup(contents)
         details = soup.findAll(
             attrs={"class": re.compile(r"commit-details|commit-comment")})
         expected_texts = list(reversed([
@@ -252,6 +272,8 @@
         self.assertEqual(
             [canonical_url(mp)],
             [link["href"] for link in details[5].findAll("a")])
+        self.assertThat(
+            contents, Not(soupmatchers.HTMLContains(MissingCommitsNote())))
 
     def _test_all_commits_link(self, branch_name, encoded_branch_name=None):
         if encoded_branch_name is None:
@@ -312,3 +334,22 @@
         with StormStatementRecorder() as recorder:
             view.landing_targets
         self.assertThat(recorder, HasQueryCount(Equals(12)))
+
+    def test_timeout(self):
+        # The page renders even if fetching commits times out.
+        self.useFixture(FakeLogger())
+        [ref] = self.factory.makeGitRefs()
+        log = self.makeCommitLog()
+        self.scanRef(ref, log[-1])
+        self.hosting_fixture.getLog.failure = TimeoutError
+        view = create_initialized_view(ref, "+index")
+        contents = view()
+        soup = BeautifulSoup(contents)
+        details = soup.findAll(
+            attrs={"class": re.compile(r"commit-details|commit-comment")})
+        expected_text = "%.7s...\nby\n%s\non 2015-01-%02d" % (
+            log[-1]["sha1"], log[-1]["author"]["name"], len(log))
+        self.assertEqual(
+            [expected_text], [extract_text(detail) for detail in details])
+        self.assertThat(
+            contents, soupmatchers.HTMLContains(MissingCommitsNote()))

=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py	2018-10-25 17:52:43 +0000
+++ lib/lp/code/interfaces/gitref.py	2018-11-29 12:18:57 +0000
@@ -364,7 +364,8 @@
         "yet been scanned.")
 
     def getCommits(start, limit=None, stop=None, union_repository=None,
-                   start_date=None, end_date=None, logger=None):
+                   start_date=None, end_date=None, handle_timeout=False,
+                   logger=None):
         """Get commit information from this reference.
 
         :param start: The commit to start listing from.
@@ -374,11 +375,14 @@
             this repository as well (particularly useful with `stop`).
         :param start_date: If not None, ignore commits before this date.
         :param end_date: If not None, ignore commits after this date.
+        :param handle_timeout: If True and the backend request times out,
+            synthesise commit information from what we have in the database.
         :param logger: An optional logger.
         :return: An iterable of commit information dicts.
         """
 
-    def getLatestCommits(quantity=10):
+    def getLatestCommits(quantity=10, extended_details=False, user=None,
+                         logger=None):
         """Return a specific number of the latest commits in this ref."""
 
     has_commits = Attribute("Whether this reference has any commits.")

=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py	2018-11-09 22:06:43 +0000
+++ lib/lp/code/model/gitref.py	2018-11-29 12:18:57 +0000
@@ -82,7 +82,11 @@
 from lp.services.database.stormbase import StormBase
 from lp.services.features import getFeatureFlag
 from lp.services.memcache.interfaces import IMemcacheClient
-from lp.services.timeout import urlfetch
+from lp.services.timeout import (
+    reduced_timeout,
+    TimeoutError,
+    urlfetch,
+    )
 from lp.services.utils import seconds_since_epoch
 from lp.services.webapp.interfaces import ILaunchBag
 
@@ -336,9 +340,10 @@
                 try:
                     log = json.loads(cached_log)
                 except Exception:
-                    logger.exception(
-                        "Cannot load cached log information for %s:%s; "
-                        "deleting" % (path, start))
+                    if logger is not None:
+                        logger.exception(
+                            "Cannot load cached log information for %s:%s; "
+                            "deleting" % (path, start))
                     memcache_client.delete(memcache_key)
         if log is None:
             if enable_hosting:
@@ -367,19 +372,38 @@
         return log
 
     def getCommits(self, start, limit=None, stop=None, union_repository=None,
-                   start_date=None, end_date=None, logger=None):
+                   start_date=None, end_date=None, handle_timeout=False,
+                   logger=None):
         # Circular import.
         from lp.code.model.gitrepository import parse_git_commits
 
-        log = self._getLog(
-            start, limit=limit, stop=stop, union_repository=union_repository,
-            logger=logger)
-        parsed_commits = parse_git_commits(log)
+        with reduced_timeout(1.0 if handle_timeout else 0.0):
+            try:
+                log = self._getLog(
+                    start, limit=limit, stop=stop,
+                    union_repository=union_repository, logger=logger)
+                commit_oids = [
+                    commit["sha1"] for commit in log if "sha1" in commit]
+                parsed_commits = parse_git_commits(log)
+            except TimeoutError:
+                if not handle_timeout:
+                    raise
+                if logger is not None:
+                    logger.exception(
+                        "Timeout fetching commits for %s" % self.identity)
+                # Synthesise a response based on what we have in the database.
+                commit_oids = [self.commit_sha1]
+                tip = {"sha1": self.commit_sha1, "synthetic": True}
+                optional_fields = (
+                    "author", "author_date", "committer", "committer_date",
+                    "commit_message")
+                for field in optional_fields:
+                    if getattr(self, field) is not None:
+                        tip[field] = getattr(self, field)
+                parsed_commits = {self.commit_sha1: tip}
         commits = []
-        for commit in log:
-            if "sha1" not in commit:
-                continue
-            parsed_commit = parsed_commits[commit["sha1"]]
+        for commit_oid in commit_oids:
+            parsed_commit = parsed_commits[commit_oid]
             author_date = parsed_commit.get("author_date")
             if start_date is not None:
                 if author_date is None or author_date < start_date:
@@ -390,8 +414,11 @@
             commits.append(parsed_commit)
         return commits
 
-    def getLatestCommits(self, quantity=10, extended_details=False, user=None):
-        commits = self.getCommits(self.commit_sha1, limit=quantity)
+    def getLatestCommits(self, quantity=10, extended_details=False, user=None,
+                         logger=None):
+        commits = self.getCommits(
+            self.commit_sha1, limit=quantity, handle_timeout=True,
+            logger=logger)
         if extended_details:
             # XXX cjwatson 2016-05-09: Add support for linked bugtasks once
             # those are supported for Git.

=== modified file 'lib/lp/code/templates/git-macros.pt'
--- lib/lp/code/templates/git-macros.pt	2018-08-24 16:52:27 +0000
+++ lib/lp/code/templates/git-macros.pt	2018-11-29 12:18:57 +0000
@@ -135,6 +135,11 @@
       </tal:diff-expander>
     </tal:ajax-revision-diffs>
   </dl>
+  <tal:synthetic
+      condition="python: any(commit_info.get('synthetic')
+                             for commit_info in commit_infos)">
+    <div>Some recent commit information could not be fetched.</div>
+  </tal:synthetic>
 
 </metal:ref-commits>
 


Follow ups