Merge lp:~julian-edwards/launchpad/hanging-builder-edit-bug-583905 into lp:launchpad

Proposed by Julian Edwards
Status: Merged
Merged at revision: 10930
Proposed branch: lp:~julian-edwards/launchpad/hanging-builder-edit-bug-583905
Merge into: lp:launchpad
Diff against target: 76 lines (+60/-1)
2 files modified
lib/lp/soyuz/browser/builder.py (+11/-1)
lib/lp/soyuz/browser/tests/test_builder_views.py (+49/-0)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/hanging-builder-edit-bug-583905
Reviewer Review Type Date Requested Status
Michael Nelson (community) code Approve
Review via email: mp+26039@code.launchpad.net

Description of the change

= Summary =
Stop timeouts on Builder:+edit

== Pre-implementation notes ==
It turns out that when the edit form is saved, the Snapshot stuff is kicked
off to grab the old object state to provide in event notifications. The
problem is that one of the properties it queries (is_available) is actually
some code that tries to send an xmlrpc request to the build slave itself.
Because this request is coming from a webapp, it hits a fairly solid firewall
until the connection times out.

== Implementation details ==
I've added notify_modified=False to the call to updateContextFromData() which
stops it from taking snapshots of the object state. The comment in the code
explains the rationale, I've repeated it here:

        # notify_modified is set False here because it uses
        # lazr.lifecycle.snapshot to store the state of the object
        # before and after modification. This is dangerous for the
        # builder model class because it causes some properties to be
        # queried that try and communicate with the slave, which cannot
        # be done from the webapp (it's generally firewalled). We could
        # prevent snapshots for individual properties by defining the
        # interface properties with doNotSnapshot() but this doesn't
        # guard against future properties being created.

== Tests ==
I've added a new unit test for builder views. It stubs out the call to a
method slaveStatusSentence() which is called by is_available, and sets
slave_called to True so we can assert whether it was called or not.

== Demo and Q/A ==
There's no way of QA-ing this since it's an artefact of the production
firewall set-up. The dogfood builders are not firewalled in the same way.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/soyuz/browser/tests/test_builder_views.py
  lib/lp/soyuz/browser/builder.py

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :

16:40 < noodles775> bigjools: why do you remove the proxy on the test case's instance var, rather than just when you need to set something that you normally wouldn't be able to?
16:45 < noodles775> bigjools: also, you could use FakeMethod there if you wanted to... it would mean that you don't need to set slave_called on the builder, which would also mean you don't need removeSecurityProxy right?
16:46 < bigjools> noodles775: proxy is removed because...ummm I can't remember, let me try without it again since I've changed the code a bit since I added it (for good reason at the time)
16:47 < noodles775> I'm guessing it's because you're setting builder.slave_called, which isn't a interface attribute (well, it's not an attribute at all except for your test is it?)
16:47 < bigjools> indeed
16:47 < bigjools> ah, it's because I can't override slaveStatusSentence otherwise
16:48 < bigjools> well, I suppose I could name it the same
16:49 < noodles775> Could you do the following in setUp: removeSecurityProxy(self.builder).slaveStatusSentence = FakeMethod(...)
16:50 < noodles775> Then your test could just check self.builder.slaveStatusSentence.call_count == 0
16:50 < bigjools> yeah, let me try it, I wasn't aware of FakeMethod
16:51 < noodles775> OK, with that r=me

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/browser/builder.py'
2--- lib/lp/soyuz/browser/builder.py 2010-04-09 15:46:09 +0000
3+++ lib/lp/soyuz/browser/builder.py 2010-05-27 09:35:43 +0000
4@@ -335,7 +335,17 @@
5 @action(_('Change'), name='update')
6 def change_details(self, action, data):
7 """Update the builder with the data from the form."""
8- builder_was_modified = self.updateContextFromData(data)
9+ # notify_modified is set False here because it uses
10+ # lazr.lifecycle.snapshot to store the state of the object
11+ # before and after modification. This is dangerous for the
12+ # builder model class because it causes some properties to be
13+ # queried that try and communicate with the slave, which cannot
14+ # be done from the webapp (it's generally firewalled). We could
15+ # prevent snapshots for individual properties by defining the
16+ # interface properties with doNotSnapshot() but this doesn't
17+ # guard against future properties being created.
18+ builder_was_modified = self.updateContextFromData(
19+ data, notify_modified=False)
20
21 if builder_was_modified:
22 notification = 'The builder "%s" was updated successfully.' % (
23
24=== added file 'lib/lp/soyuz/browser/tests/test_builder_views.py'
25--- lib/lp/soyuz/browser/tests/test_builder_views.py 1970-01-01 00:00:00 +0000
26+++ lib/lp/soyuz/browser/tests/test_builder_views.py 2010-05-27 09:35:43 +0000
27@@ -0,0 +1,49 @@
28+# Copyright 2009 Canonical Ltd. This software is licensed under the
29+# GNU Affero General Public License version 3 (see the file LICENSE).
30+
31+__metaclass__ = type
32+
33+from zope.security.proxy import removeSecurityProxy
34+from canonical.launchpad.ftests import login
35+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
36+from canonical.testing import LaunchpadFunctionalLayer
37+from lp.soyuz.browser.builder import BuilderEditView
38+from lp.testing import TestCaseWithFactory
39+from lp.testing.fakemethod import FakeMethod
40+
41+
42+class TestBuilderEditView(TestCaseWithFactory):
43+
44+ layer = LaunchpadFunctionalLayer
45+
46+ def setUp(self):
47+ super(TestBuilderEditView, self).setUp()
48+ # Login as an admin to ensure access to the view's context
49+ # object.
50+ login('admin@canonical.com')
51+ self.builder = removeSecurityProxy(self.factory.makeBuilder())
52+
53+ def initialize_view(self):
54+ form = {
55+ "field.manual" : "on",
56+ "field.actions.update" : "Change",
57+ }
58+ request = LaunchpadTestRequest(method="POST", form=form)
59+ view = BuilderEditView(self.builder, request)
60+ return view
61+
62+ def test_posting_form_doesnt_call_slave_xmlrpc(self):
63+ # Posting the +edit for should not call is_available, which
64+ # would do xmlrpc to a slave builder and is explicitly forbidden
65+ # in a webapp process.
66+ view = self.initialize_view()
67+
68+ # Stub out the slaveStatusSentence() method with a fake one that
69+ # records if it's been called.
70+ view.context.slaveStatusSentence = FakeMethod(result=[0])
71+
72+ view.initialize()
73+
74+ # If the dummy slaveStatusSentence() was called the call count
75+ # would not be zero.
76+ self.assertTrue(view.context.slaveStatusSentence.call_count == 0 )