← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/squash-cw-protocolerrors into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/squash-cw-protocolerrors into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #719738 Broken remote bug trackers should not cause OOPSes
  https://bugs.launchpad.net/bugs/719738

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/squash-cw-protocolerrors/+merge/51680

checkwatches regularly sees down or otherwise broken remote bug trackers. These often manifest as xmlrpc.ProtocolErrors when attempting to detect which ExternalBugTracker to use, which end up logged as OOPSes (bug #719738). This makes up the largest share of checkwatches' OOPSes.

This branch catches them lower, notifies each relevant watch of failure, and logs the error at INFO without OOPSing.

One minor note: bug #390543 is also half-fixed; occurrences are now logged at INFO, as well as having an OOPS logged. It made sense to reuse that code path for handling ProtocolErrors too, but the OOPSes cannot be removed until a later branch significantly refactors tests.
-- 
https://code.launchpad.net/~wgrant/launchpad/squash-cw-protocolerrors/+merge/51680
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/squash-cw-protocolerrors into lp:launchpad.
=== modified file 'lib/lp/bugs/doc/checkwatches-cli-switches.txt'
--- lib/lp/bugs/doc/checkwatches-cli-switches.txt	2010-12-24 16:39:02 +0000
+++ lib/lp/bugs/doc/checkwatches-cli-switches.txt	2011-03-01 02:37:58 +0000
@@ -107,6 +107,7 @@
     DEBUG Enabled by DEFAULT section
     INFO Resetting 5 bug watches for bug tracker 'savannah'
     INFO Updating 5 watches on bug tracker 'savannah'
+    INFO 'Unsupported Bugtracker' error updating http://savannah.gnu.org/: SAVANE
     WARNING ExternalBugtracker for BugTrackerType 'SAVANE' is not known.
     (OOPS-...)
     INFO 0 watches left to check on bug tracker 'savannah'

=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
--- lib/lp/bugs/doc/checkwatches.txt	2011-02-14 23:34:02 +0000
+++ lib/lp/bugs/doc/checkwatches.txt	2011-03-01 02:37:58 +0000
@@ -52,6 +52,7 @@
     DEBUG   No watches to update on http://bugzilla.gnome.org/
     DEBUG   No watches to update on http://bugzilla.gnome.org/bugs
     DEBUG   No watches to update on https://bugzilla.mozilla.org/
+    INFO    'Unsupported Bugtracker' error updating http://savannah.gnu.org/: SAVANE
     WARNING ExternalBugtracker for BugTrackerType 'SAVANE' is not known.
     (OOPS-...)
     DEBUG   No watches to update on http://sourceforge.net/
@@ -273,7 +274,7 @@
     ...     urllib2.urlopen = urlopen
     DEBUG No global batch size specified.
     INFO Updating 5 watches for 5 bugs on http://bugs.example.com
-    INFO Connection timed out when updating ... (OOPS-...)
+    INFO Connection timed out when updating ...
 
 
 Updating all the watches on a bug tracker
@@ -318,6 +319,7 @@
     >>> update_all('savannah', batch_size)
     INFO Resetting 1 bug watches for bug tracker 'savannah'
     INFO Updating 1 watches on bug tracker 'savannah'
+    INFO 'Unsupported Bugtracker' error updating http://savannah.gnu.org/: SAVANE
     WARNING ExternalBugtracker for BugTrackerType 'SAVANE' is not known.
     (OOPS-...)
     INFO 0 watches left to check on bug tracker 'savannah'

=== modified file 'lib/lp/bugs/scripts/checkwatches/core.py'
--- lib/lp/bugs/scripts/checkwatches/core.py	2010-12-02 16:13:51 +0000
+++ lib/lp/bugs/scripts/checkwatches/core.py	2011-03-01 02:37:58 +0000
@@ -27,6 +27,7 @@
 import sys
 import threading
 import time
+from xmlrpclib import ProtocolError
 
 import pytz
 from twisted.internet import reactor
@@ -404,7 +405,7 @@
             try:
                 trackers_and_watches = self._getExternalBugTrackersAndWatches(
                     bug_tracker, bug_watches_to_update)
-            except UnknownBugTrackerTypeError, error:
+            except (UnknownBugTrackerTypeError, ProtocolError), error:
                 # We update all the bug watches to reflect the fact that
                 # this error occurred. We also update their last checked
                 # date to ensure that they don't get checked for another
@@ -414,10 +415,17 @@
                 with self.transaction:
                     getUtility(IBugWatchSet).bulkSetError(
                         bug_watches_to_update, error_type)
-                message = (
-                    "ExternalBugtracker for BugTrackerType '%s' is not "
-                    "known." % (error.bugtrackertypename))
-                self.warning(message)
+                    self.logger.info(
+                        "'%s' error updating %s: %s" % (
+                            error_type.title, bug_tracker.baseurl, error))
+                # This is retained to make tests happy. It will be
+                # removed later when the checkwatches OOPS
+                # infrastructure can be.
+                if isinstance(error, UnknownBugTrackerTypeError):
+                    message = (
+                        "ExternalBugtracker for BugTrackerType '%s' is not "
+                        "known." % (error.bugtrackertypename))
+                    self.warning(message)
             else:
                 for remotesystem, bug_watch_batch in trackers_and_watches:
                     self.updateBugWatches(

=== modified file 'lib/lp/bugs/scripts/checkwatches/tests/test_core.py'
--- lib/lp/bugs/scripts/checkwatches/tests/test_core.py	2010-12-20 03:21:03 +0000
+++ lib/lp/bugs/scripts/checkwatches/tests/test_core.py	2011-03-01 02:37:58 +0000
@@ -7,6 +7,7 @@
 from datetime import datetime
 import threading
 import unittest
+from xmlrpclib import ProtocolError
 
 import transaction
 from zope.component import getUtility
@@ -26,6 +27,7 @@
     BugTrackerType,
     IBugTrackerSet,
     )
+from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
 from lp.bugs.scripts import checkwatches
 from lp.bugs.scripts.checkwatches.base import (
     CheckWatchesErrorUtility,
@@ -164,6 +166,15 @@
         self.failIf(remote_system.sync_comments)
 
 
+class BrokenCheckwatchesMaster(CheckwatchesMaster):
+
+    error_code = None
+
+    def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches):
+        raise ProtocolError(
+            "http://example.com/";, self.error_code, "Borked", "")
+
+
 class TestCheckwatchesMaster(TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer
@@ -176,12 +187,7 @@
         # Regression test for bug 497141. KeyErrors raised in
         # RemoteBugUpdater.updateRemoteBug() shouldn't cause
         # checkwatches to abort.
-
-        # Create a couple of bug watches for testing purposes.
-        bug_tracker = self.factory.makeBugTracker()
-        bug_watches = [
-            self.factory.makeBugWatch(bugtracker=bug_tracker)
-            for i in range(2)]
+        (bug_tracker, bug_watches) = self.factory.makeBugTrackerWithWatches()
 
         # Use a test XML-RPC transport to ensure no connections happen.
         test_transport = TestBugzillaAPIXMLRPCTransport(bug_tracker.baseurl)
@@ -228,6 +234,46 @@
         checkwatches.core.suggest_batch_size(remote_system, 99999)
         self.failUnlessEqual(247, remote_system.batch_size)
 
+    def test_xmlrpc_connection_errors_set_activity_properly(self):
+        # HTTP status codes of 502, 503 and 504 indicate connection
+        # errors. An XML-RPC request that fails with one of those is
+        # logged as a connection failure, not an OOPS.
+        master = BrokenCheckwatchesMaster(
+            transaction.manager, logger=BufferLogger())
+        master.error_code = 503
+        (bug_tracker, bug_watches) = self.factory.makeBugTrackerWithWatches(
+            base_url='http://example.com/')
+        transaction.commit()
+        master._updateBugTracker(bug_tracker)
+        for bug_watch in bug_watches:
+            self.assertEquals(
+                BugWatchActivityStatus.CONNECTION_ERROR,
+                bug_watch.last_error_type)
+        self.assertEqual(
+            "INFO 'Connection Error' error updating http://example.com/: "
+            "<ProtocolError for http://example.com/: 503 Borked>\n",
+            master.logger.getLogBuffer())
+
+    def test_xmlrpc_other_errors_set_activity_properly(self):
+        # HTTP status codes that indicate anything other than a
+        # connection error still aren't OOPSes. They are logged as an
+        # unknown error instead.
+        master = BrokenCheckwatchesMaster(
+            transaction.manager, logger=BufferLogger())
+        master.error_code = 403
+        (bug_tracker, bug_watches) = self.factory.makeBugTrackerWithWatches(
+            base_url='http://example.com/')
+        transaction.commit()
+        master._updateBugTracker(bug_tracker)
+        for bug_watch in bug_watches:
+            self.assertEquals(
+                BugWatchActivityStatus.UNKNOWN,
+                bug_watch.last_error_type)
+        self.assertEqual(
+            "INFO 'Unknown' error updating http://example.com/: "
+            "<ProtocolError for http://example.com/: 403 Borked>\n",
+            master.logger.getLogBuffer())
+
 
 class TestUpdateBugsWithLinkedQuestions(unittest.TestCase):
     """Tests for updating bugs with linked questions."""

=== modified file 'lib/lp/bugs/scripts/checkwatches/utilities.py'
--- lib/lp/bugs/scripts/checkwatches/utilities.py	2011-01-19 00:10:48 +0000
+++ lib/lp/bugs/scripts/checkwatches/utilities.py	2011-03-01 02:37:58 +0000
@@ -10,6 +10,7 @@
     ]
 
 import socket
+from xmlrpclib import ProtocolError
 
 from lp.bugs.externalbugtracker import (
     BugNotFound,
@@ -42,6 +43,13 @@
 
 def get_bugwatcherrortype_for_error(error):
     """Return the correct `BugWatchActivityStatus` for a given error."""
+    # ProtocolError is rather generic, but always indicates a server
+    # failure. 502 Bad Gateway, 503 Service Unavailable and 504 Gateway
+    # Timeout are connection errors, while the rest are unknown.
+    if isinstance(error, ProtocolError):
+        if error.errcode in (502, 503, 504):
+            return BugWatchActivityStatus.CONNECTION_ERROR
+
     for exc_type, bugwatcherrortype in _exception_to_bugwatcherrortype:
         if isinstance(error, exc_type):
             return bugwatcherrortype

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-02-25 14:59:44 +0000
+++ lib/lp/testing/factory.py	2011-03-01 02:37:58 +0000
@@ -1673,6 +1673,14 @@
         return getUtility(IBugTrackerSet).ensureBugTracker(
             base_url, owner, bugtrackertype, title=title, name=name)
 
+    def makeBugTrackerWithWatches(self, base_url=None, count=2):
+        """Make a new bug tracker with some watches."""
+        bug_tracker = self.makeBugTracker(base_url=base_url)
+        bug_watches = [
+            self.makeBugWatch(bugtracker=bug_tracker)
+            for i in range(count)]
+        return (bug_tracker, bug_watches)
+
     def makeBugTrackerComponentGroup(self, name=None, bug_tracker=None):
         """Make a new bug tracker component group."""
         if name is None: