Merge lp:~gary/launchpad/bug744888 into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 12829
Proposed branch: lp:~gary/launchpad/bug744888
Merge into: lp:launchpad
Diff against target: 146 lines (+74/-6)
3 files modified
lib/lp/bugs/interfaces/bug.py (+4/-4)
lib/lp/bugs/model/bug.py (+3/-0)
lib/lp/bugs/tests/test_bug.py (+67/-2)
To merge this branch: bzr merge lp:~gary/launchpad/bug744888
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+57499@code.launchpad.net

Commit message

[r=abentley][bug=744888] Snapshots of a bug should not get all of its messages. webservice bug.mute() should work.

Description of the change

This branch hopefully fixes the timeout when muting bug 1 via the webservice. Thanks to investigation by Graham and Robert, the problem appeared to be the fact that, when the webservice made snapshots made snapshots, we got all of the bugs comments ("messages"), and this extra weight pushed us over the timeout. We did that because we snapshotted "messages" and "attachments". Bug 1 has over 1400 comments as of this writing. Robert suggested that simply indicating that these attributes should not be included in snapshots should be sufficient to fix the problem. My branch implements that solution. An ec2 test run seems to indicate that it won't cause any problems.

The most questionable aspect of this is the test. I'll leave the comments to speak for themselves.

Along the way, when I was testing the problem, I discovered that the bug's mute method in the web API was not working as expected. This was getting in the way of my work. I introduced a fix for that in this branch, as well as a test to show that it and unmute both work as the webservice exposes them (unmute already did).

Thank you!

Gary

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

If StormStatementRecorder works for your test, I recommend using that instead.

Please change the SQL check as discussed on IRC:
<abentley> gary_poster: I wonder whether there's risk of your SQL check hitting false negatives? Would it be crazy to just look for the string 'message' in the entire statement?
<gary_poster> abentley: looking for message: well...there are also "bugmessage"s in theory...it's obviously a balance between possible false negatives and false positives.
<gary_poster> I guess we could try being a bit more aggressive. I'd be OK with simply splitting on whitespace and making sure there are no strings that start with "message" (which would include tables). If people find that too restrictive in the future they can adjust the test.

Aside from that, looks fine.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2011-04-05 17:53:52 +0000
+++ lib/lp/bugs/interfaces/bug.py 2011-04-13 21:05:25 +0000
@@ -294,11 +294,11 @@
294 title=_("List of bug attachments."),294 title=_("List of bug attachments."),
295 value_type=Reference(schema=IBugAttachment),295 value_type=Reference(schema=IBugAttachment),
296 readonly=True)296 readonly=True)
297 attachments = exported(297 attachments = doNotSnapshot(exported(
298 CollectionField(298 CollectionField(
299 title=_("List of bug attachments."),299 title=_("List of bug attachments."),
300 value_type=Reference(schema=IBugAttachment),300 value_type=Reference(schema=IBugAttachment),
301 readonly=True))301 readonly=True)))
302 questions = Attribute("List of questions related to this bug.")302 questions = Attribute("List of questions related to this bug.")
303 specifications = Attribute("List of related specifications.")303 specifications = Attribute("List of related specifications.")
304 linked_branches = exported(304 linked_branches = exported(
@@ -394,13 +394,13 @@
394 readonly=True,394 readonly=True,
395 value_type=Reference(schema=IMessage)))395 value_type=Reference(schema=IMessage)))
396396
397 indexed_messages = exported(397 indexed_messages = doNotSnapshot(exported(
398 CollectionField(398 CollectionField(
399 title=_("The messages related to this object, in reverse "399 title=_("The messages related to this object, in reverse "
400 "order of creation (so newest first)."),400 "order of creation (so newest first)."),
401 readonly=True,401 readonly=True,
402 value_type=Reference(schema=IMessage)),402 value_type=Reference(schema=IMessage)),
403 exported_as='messages')403 exported_as='messages'))
404404
405 def _indexed_messages(include_content=False, include_parents=False):405 def _indexed_messages(include_content=False, include_parents=False):
406 """Low level query for getting bug messages.406 """Low level query for getting bug messages.
407407
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-04-12 13:17:34 +0000
+++ lib/lp/bugs/model/bug.py 2011-04-13 21:05:25 +0000
@@ -850,6 +850,9 @@
850850
851 def mute(self, person, muted_by):851 def mute(self, person, muted_by):
852 """See `IBug`."""852 """See `IBug`."""
853 if person is None:
854 # This may be a webservice request.
855 person = muted_by
853 # If there's an existing subscription, update it.856 # If there's an existing subscription, update it.
854 store = Store.of(self)857 store = Store.of(self)
855 subscriptions = store.find(858 subscriptions = store.find(
856859
=== modified file 'lib/lp/bugs/tests/test_bug.py'
--- lib/lp/bugs/tests/test_bug.py 2011-03-23 16:28:51 +0000
+++ lib/lp/bugs/tests/test_bug.py 2011-04-13 21:05:25 +0000
@@ -5,16 +5,19 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from lazr.lifecycle.snapshot import Snapshot
9from zope.interface import providedBy
10
8from canonical.testing.layers import DatabaseFunctionalLayer11from canonical.testing.layers import DatabaseFunctionalLayer
912
10from lp.bugs.enum import BugNotificationLevel13from lp.bugs.enum import BugNotificationLevel
11from lp.testing import (14from lp.testing import (
12 feature_flags,
13 person_logged_in,15 person_logged_in,
14 set_feature_flag,16 StormStatementRecorder,
15 TestCaseWithFactory,17 TestCaseWithFactory,
16 )18 )
1719
20
18class TestBugSubscriptionMethods(TestCaseWithFactory):21class TestBugSubscriptionMethods(TestCaseWithFactory):
19 layer = DatabaseFunctionalLayer22 layer = DatabaseFunctionalLayer
2023
@@ -55,6 +58,15 @@
55 BugNotificationLevel.NOTHING,58 BugNotificationLevel.NOTHING,
56 muted_subscription.bug_notification_level)59 muted_subscription.bug_notification_level)
5760
61 def test_mute_mutes_muter(self):
62 # When exposed in the web API, the mute method regards the
63 # first, `person` argument as optional, and the second
64 # `muted_by` argument is supplied from the request. In this
65 # case, the person should be the muter.
66 with person_logged_in(self.person):
67 self.bug.mute(None, self.person)
68 self.assertTrue(self.bug.isMuted(self.person))
69
58 def test_mute_mutes_user_with_existing_subscription(self):70 def test_mute_mutes_user_with_existing_subscription(self):
59 # Bug.mute() will update an existing subscription so that it71 # Bug.mute() will update an existing subscription so that it
60 # becomes muted.72 # becomes muted.
@@ -74,3 +86,56 @@
74 self.assertTrue(self.bug.isMuted(self.person))86 self.assertTrue(self.bug.isMuted(self.person))
75 self.bug.unmute(self.person, self.person)87 self.bug.unmute(self.person, self.person)
76 self.assertFalse(self.bug.isMuted(self.person))88 self.assertFalse(self.bug.isMuted(self.person))
89
90 def test_unmute_mutes_unmuter(self):
91 # When exposed in the web API, the unmute method regards the
92 # first, `person` argument as optional, and the second
93 # `unmuted_by` argument is supplied from the request. In this
94 # case, the person should be the muter.
95 with person_logged_in(self.person):
96 self.bug.mute(self.person, self.person)
97 self.bug.unmute(None, self.person)
98 self.assertFalse(self.bug.isMuted(self.person))
99
100
101class TestBugSnapshotting(TestCaseWithFactory):
102 layer = DatabaseFunctionalLayer
103
104 def setUp(self):
105 super(TestBugSnapshotting, self).setUp()
106 self.bug = self.factory.makeBug()
107 self.person = self.factory.makePerson()
108
109 def test_bug_snapshot_does_not_include_messages(self):
110 # A snapshot of a bug does not include its messages or
111 # attachments (which get the messages from the database). If it
112 # does, the webservice can become unusable if changes are made
113 # to bugs with many comments, such as bug 1. See, for instance,
114 # bug 744888. This test is primarily to keep the problem from
115 # slipping in again. To do so, we resort to somewhat
116 # extraordinary measures. In addition to verifying that the
117 # snapshot does not have the attributes that currently trigger
118 # the problem, we also actually look at the SQL that is
119 # generated by creating the snapshot. With this, we can verify
120 # that the Message table is not included. This is ugly, but
121 # this has a chance of fighting against future eager loading
122 # optimizations that might trigger the problem again.
123 with person_logged_in(self.person):
124 with StormStatementRecorder() as recorder:
125 snapshot = Snapshot(self.bug, providing=providedBy(self.bug))
126 sql_statements = recorder.statements
127 # This uses "self" as a marker to show that the attribute does not
128 # exist. We do not use hasattr because it eats exceptions.
129 #self.assertTrue(getattr(snapshot, 'messages', self) is self)
130 #self.assertTrue(getattr(snapshot, 'attachments', self) is self)
131 for sql in sql_statements:
132 # We are going to be aggressive about looking for the problem in
133 # the SQL. We'll split the SQL up by whitespace, and then look
134 # for strings that start with "message". If that is too
135 # aggressive in the future from some reason, please do adjust the
136 # test appropriately.
137 sql_tokens = sql.lower().split()
138 self.assertEqual(
139 [token for token in sql_tokens
140 if token.startswith('message')],
141 [])