Merge lp:~sinzui/launchpad/prf-connection-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 12371
Proposed branch: lp:~sinzui/launchpad/prf-connection-0
Merge into: lp:launchpad
Diff against target: 257 lines (+40/-47)
2 files modified
lib/lp/registry/scripts/productreleasefinder/walker.py (+10/-4)
lib/lp/registry/tests/test_prf_walker.py (+30/-43)
To merge this branch: bzr merge lp:~sinzui/launchpad/prf-connection-0
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Benji York (community) code* Approve
Review via email: mp+49456@code.launchpad.net

Description of the change

Recover from an open() failure when the product-release-finder calls walk().

    Launchpad bug: https://bugs.launchpad.net/bugs/717363
    Pre-implementation: no one
    Test command: ./bin/test -vv -t test_prf_walker

The product release finder failed on the first use the an FTPWalker. This is
probably an operational issue where the site or the port is blocked.
Regardless of the connectivity issue, the PRF should not fail.

From the prf log:
2011-02-11 07:22:36 INFO Connecting to ftp.gnu.org
2011-02-11 07:22:36 ERROR Unhandled exception

From the tb in the librarian
  File "/srv/launchpad.net/production/launchpad-rev-12335/lib/lp/registry/scripts/productreleasefinder/walker.py", line 126, in walk
    self.open()
  File "/srv/launchpad.net/production/launchpad-rev-12335/lib/lp/registry/scripts/productreleasefinder/walker.py", line 206, in open
    self.ftp.connect(self.host)
  File "/usr/lib/python2.6/ftplib.py", line 131, in connect
    self.sock = socket.create_connection((self.host, self.port), self.timeout)
  File "/usr/lib/python2.6/socket.py", line 514, in create_connection
    raise error, msg
error: [Errno 111] Connection refused

--------------------------------------------------------------------

RULES

    * Catch the exception raise by self.open(), log the issue and return
      early.
    * ADDENDUM: The local imports are not needed in the tests now that
      cyclic import issues were fixed.

QA

    If ftp.gnu.org is blocking Lp:
        * Run the product-release finder on staging and verify that the PRF
          completes. (The production instance is dying in the first 5 minutes).
    otherwise:
        * Hope for the best.

LINT

    lib/lp/registry/scripts/productreleasefinder/walker.py
    lib/lp/registry/tests/test_prf_walker.py

^ lint hates the long lines in the test data. I can clean this up, but I think
it will make the test more difficult to read, so I chose it ignore it.

IMPLEMENTATION

I removed the local import from the test. I added a new test to instrument
an exception from open(). The prf often checks for (IOError, socket.error)
though I believe that in the known case, only socket.error will be raised.
I also fixed some deprecated uses of raise reported by lint.
    lib/lp/registry/scripts/productreleasefinder/walker.py
    lib/lp/registry/tests/test_prf_walker.py

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

Approved* (Brad will have to review my review since I'm doing mentored
reviews.)

The test (like the rest of the branch) is nice and clean. I almost
suggested using list(walker) instead of the (apparently) do-nothing
loop, but I'm not sure if using list() is just clearer to me and most
people would understand the intent of the current code better.

I especially like the fact that the WalkerBase import can be moved to
the top of the test module. Much nicer now.

review: Approve (code*)
Revision history for this message
Brad Crittenden (bac) wrote :

I agree with Benji's suggestion to use list() as the dummy loop looks a bit silly. Using list() with a comment is even better:

# Listify the walker generator, which will log the expected error.
list(walker)

And thanks a zillion for the clean up!

review: Approve (code)
Revision history for this message
Robert Collins (lifeless) wrote :

You should either log an error (if we want OOPS for these - we
probably don't as they are user data : but how will users debug a
fail? - I think we need a new bug for that), or log the exception in
the message so that someone reading the log can determine the error it
experienced.

Revision history for this message
Curtis Hovey (sinzui) wrote :

We do not want to log an error for user data: The issue of getting feedback is covered in bug 70524. It is out of scope for this issue.

Revision history for this message
Robert Collins (lifeless) wrote :

On Sat, Feb 12, 2011 at 10:29 AM, Curtis Hovey
<email address hidden> wrote:
> We do not want to log an error for user data: The issue of getting feedback is covered in bug 70524. It is out of scope for this issue.

Agreed; am glad there is a bug already. But we should log /what the
error was/ not just that there was an error. e.g

except (IOError, OSError), e:
   log("failure %s" % e)

(done minimally here)

Revision history for this message
Curtis Hovey (sinzui) wrote :

Agreed and done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/scripts/productreleasefinder/walker.py'
2--- lib/lp/registry/scripts/productreleasefinder/walker.py 2010-08-20 20:31:18 +0000
3+++ lib/lp/registry/scripts/productreleasefinder/walker.py 2011-02-12 00:03:40 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """HTTP and FTP walker.
10@@ -92,7 +92,7 @@
11 (scheme, netloc, path, query, fragment) \
12 = urlsplit(base, self.URL_SCHEMES[0], self.FRAGMENTS)
13 if scheme not in self.URL_SCHEMES:
14- raise WalkerError, "Can't handle %s scheme" % scheme
15+ raise WalkerError("Can't handle %s scheme" % scheme)
16 self.scheme = scheme
17 self.full_netloc = netloc
18
19@@ -123,7 +123,12 @@
20 Yields (dirpath, dirnames, filenames) for each path under the base;
21 dirnames can be modified as with os.walk.
22 """
23- self.open()
24+ try:
25+ self.open()
26+ except (IOError, socket.error), e:
27+ self.log.info("Could not connect to %s" % self.base)
28+ self.log.info("Failure: %s" % e)
29+ return
30
31 subdirs = [self.path]
32 while len(subdirs):
33@@ -423,7 +428,8 @@
34 elif scheme in ["file"]:
35 return os.walk(path)
36 else:
37- raise WalkerError, "Unknown scheme: %s" % scheme
38+ raise WalkerError("Unknown scheme: %s" % scheme)
39+
40
41 def combine_url(base, subdir, filename):
42 """Combine a URL from the three parts returned by walk()."""
43
44=== modified file 'lib/lp/registry/tests/test_prf_walker.py'
45--- lib/lp/registry/tests/test_prf_walker.py 2010-08-20 20:31:18 +0000
46+++ lib/lp/registry/tests/test_prf_walker.py 2011-02-12 00:03:40 +0000
47@@ -1,4 +1,4 @@
48-# Copyright 2009 Canonical Ltd. This software is licensed under the
49+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
50 # GNU Affero General Public License version 3 (see the file LICENSE).
51
52 """Tests for lp.registry.scripts.productreleasefinder.walker."""
53@@ -8,8 +8,10 @@
54 import unittest
55 import urlparse
56
57+
58 from canonical.lazr.utils import safe_hasattr
59 from canonical.testing import reset_logging
60+from lp.registry.scripts.productreleasefinder.walker import WalkerBase
61 from lp.testing import TestCase
62
63
64@@ -17,16 +19,12 @@
65
66 def testCreatesDefaultLogger(self):
67 """WalkerBase creates a default logger."""
68- from lp.registry.scripts.productreleasefinder.walker import (
69- WalkerBase)
70 from logging import Logger
71 w = WalkerBase("/")
72 self.failUnless(isinstance(w.log, Logger))
73
74 def testCreatesChildLogger(self):
75 """WalkerBase creates a child logger if given a parent."""
76- from lp.registry.scripts.productreleasefinder.walker import (
77- WalkerBase)
78 from logging import getLogger
79 parent = getLogger("foo")
80 w = WalkerBase("/", log_parent=parent)
81@@ -37,29 +35,21 @@
82
83 def testSetsBase(self):
84 """WalkerBase sets the base property."""
85- from lp.registry.scripts.productreleasefinder.walker import (
86- WalkerBase)
87 w = WalkerBase("ftp://localhost/")
88 self.assertEquals(w.base, "ftp://localhost/")
89
90 def testSetsScheme(self):
91 """WalkerBase sets the scheme property."""
92- from lp.registry.scripts.productreleasefinder.walker import (
93- WalkerBase)
94 w = WalkerBase("ftp://localhost/")
95 self.assertEquals(w.scheme, "ftp")
96
97 def testSetsHost(self):
98 """WalkerBase sets the host property."""
99- from lp.registry.scripts.productreleasefinder.walker import (
100- WalkerBase)
101 w = WalkerBase("ftp://localhost/")
102 self.assertEquals(w.host, "localhost")
103
104 def testNoScheme(self):
105 """WalkerBase works when given a URL with no scheme."""
106- from lp.registry.scripts.productreleasefinder.walker import (
107- WalkerBase)
108 w = WalkerBase("/")
109 self.assertEquals(w.host, "")
110
111@@ -71,45 +61,33 @@
112
113 def testUnescapesHost(self):
114 """WalkerBase unescapes the host portion."""
115- from lp.registry.scripts.productreleasefinder.walker import (
116- WalkerBase)
117 w = WalkerBase("ftp://local%40host/")
118 self.assertEquals(w.host, "local@host")
119
120 def testNoUsername(self):
121 """WalkerBase stores None when there is no username."""
122- from lp.registry.scripts.productreleasefinder.walker import (
123- WalkerBase)
124 w = WalkerBase("ftp://localhost/")
125 self.assertEquals(w.user, None)
126
127 def testUsername(self):
128 """WalkerBase splits out the username from the host portion."""
129- from lp.registry.scripts.productreleasefinder.walker import (
130- WalkerBase)
131 w = WalkerBase("ftp://scott@localhost/")
132 self.assertEquals(w.user, "scott")
133 self.assertEquals(w.host, "localhost")
134
135 def testUnescapesUsername(self):
136 """WalkerBase unescapes the username portion."""
137- from lp.registry.scripts.productreleasefinder.walker import (
138- WalkerBase)
139 w = WalkerBase("ftp://scott%3awibble@localhost/")
140 self.assertEquals(w.user, "scott:wibble")
141 self.assertEquals(w.host, "localhost")
142
143 def testNoPassword(self):
144 """WalkerBase stores None when there is no password."""
145- from lp.registry.scripts.productreleasefinder.walker import (
146- WalkerBase)
147 w = WalkerBase("ftp://scott@localhost/")
148 self.assertEquals(w.passwd, None)
149
150 def testPassword(self):
151 """WalkerBase splits out the password from the username."""
152- from lp.registry.scripts.productreleasefinder.walker import (
153- WalkerBase)
154 w = WalkerBase("ftp://scott:wibble@localhost/")
155 self.assertEquals(w.user, "scott")
156 self.assertEquals(w.passwd, "wibble")
157@@ -117,8 +95,6 @@
158
159 def testUnescapesPassword(self):
160 """WalkerBase unescapes the password portion."""
161- from lp.registry.scripts.productreleasefinder.walker import (
162- WalkerBase)
163 w = WalkerBase("ftp://scott:wibble%20wobble@localhost/")
164 self.assertEquals(w.user, "scott")
165 self.assertEquals(w.passwd, "wibble wobble")
166@@ -126,43 +102,31 @@
167
168 def testPathOnly(self):
169 """WalkerBase stores the path if that's all there is."""
170- from lp.registry.scripts.productreleasefinder.walker import (
171- WalkerBase)
172 w = WalkerBase("/path/to/something/")
173 self.assertEquals(w.path, "/path/to/something/")
174
175 def testPathInUrl(self):
176 """WalkerBase stores the path portion of a complete URL."""
177- from lp.registry.scripts.productreleasefinder.walker import (
178- WalkerBase)
179 w = WalkerBase("ftp://localhost/path/to/something/")
180 self.assertEquals(w.path, "/path/to/something/")
181
182 def testAddsSlashToPath(self):
183 """WalkerBase adds a trailing slash to path if ommitted."""
184- from lp.registry.scripts.productreleasefinder.walker import (
185- WalkerBase)
186 w = WalkerBase("ftp://localhost/path/to/something")
187 self.assertEquals(w.path, "/path/to/something/")
188
189 def testUnescapesPath(self):
190 """WalkerBase leaves the path escaped."""
191- from lp.registry.scripts.productreleasefinder.walker import (
192- WalkerBase)
193 w = WalkerBase("ftp://localhost/some%20thing/")
194 self.assertEquals(w.path, "/some%20thing/")
195
196 def testStoresQuery(self):
197 """WalkerBase stores the query portion of a supporting URL."""
198- from lp.registry.scripts.productreleasefinder.walker import (
199- WalkerBase)
200 w = WalkerBase("http://localhost/?foo")
201 self.assertEquals(w.query, "foo")
202
203 def testStoresFragment(self):
204 """WalkerBase stores the fragment portion of a supporting URL."""
205- from lp.registry.scripts.productreleasefinder.walker import (
206- WalkerBase)
207 WalkerBase.FRAGMENTS = True
208 try:
209 w = WalkerBase("http://localhost/#foo")
210@@ -180,8 +144,6 @@
211
212 def test_walk_UnicodeEncodeError(self):
213 """Verify that a UnicodeEncodeError is logged."""
214- from lp.registry.scripts.productreleasefinder.walker import (
215- WalkerBase)
216
217 class TestWalker(WalkerBase):
218
219@@ -202,12 +164,37 @@
220 logger.setLevel(logging.DEBUG)
221 logger.addHandler(logging.StreamHandler(log_output))
222 walker = TestWalker('http://example.org/foo', logger)
223- for dummy in walker:
224- pass
225+ list(walker)
226 self.assertEqual(
227 "Unicode error parsing http://example.org/foo page '/foo/'\n",
228 log_output.getvalue())
229
230+ def test_walk_open_fail(self):
231+ # The walker handles an exception raised during open().
232+
233+ class TestWalker(WalkerBase):
234+
235+ def list(self, sub_dir):
236+ pass
237+
238+ def open(self):
239+ raise IOError("Test failure.")
240+
241+ def close(self):
242+ pass
243+
244+ log_output = StringIO.StringIO()
245+ logger = logging.getLogger()
246+ self.addCleanup(logger.setLevel, logger.level)
247+ logger.setLevel(logging.DEBUG)
248+ logger.addHandler(logging.StreamHandler(log_output))
249+ walker = TestWalker('ftp://example.org/foo', logger)
250+ list(walker)
251+ self.assertEqual(
252+ "Could not connect to ftp://example.org/foo\n"
253+ "Failure: Test failure.\n",
254+ log_output.getvalue())
255+
256
257 class FTPWalker_Base(TestCase):
258