Merge lp:~abentley/launchpad/safe-branch-upgrade into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 13036
Proposed branch: lp:~abentley/launchpad/safe-branch-upgrade
Merge into: lp:launchpad
Diff against target: 275 lines (+112/-23)
4 files modified
lib/lp/code/model/branchjob.py (+8/-4)
lib/lp/code/model/tests/test_branchjob.py (+9/-18)
lib/lp/scripts/helpers.py (+42/-1)
lib/lp/scripts/tests/test_helpers.py (+53/-0)
To merge this branch: bzr merge lp:~abentley/launchpad/safe-branch-upgrade
Reviewer Review Type Date Requested Status
Deryck Hodge (community) code Approve
Review via email: mp+60634@code.launchpad.net

Commit message

Ensure no transaction during branch upgrade

Description of the change

= Summary =
Fix bug #777958: branch upgrade jobs keep transaction open

== Proposed fix ==
Use a context manager to check for open transactions.

== Pre-implementation notes ==
Pre-and mid-implementation call with deryck.

== Implementation details ==
Implement TransactionFreeOperation context manager to use with operations that
must have no transaction active (i.e. long-running operations).

Provide a second contextmanager, TransactionFreeOperation.require, that ensures
TransactionFreeOperation is used at least once in its context.

Also, there were some drive-by lint fixes. Mostly removing unused variables.

== Tests ==
bin/test -t test_helpers -t test_upgrades_branch

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/scripts/helpers.py
  lib/lp/code/model/tests/test_branchjob.py
  lib/lp/scripts/tests/test_helpers.py
  lib/lp/code/model/branchjob.py

To post a comment you must log in.
Revision history for this message
Deryck Hodge (deryck) wrote :

This looks great. TransactionFreeOperation is a nice class for dealing with this. It might be worth sending an email to launchpad-dev about its existence, about how to use it in code and tests, and so people know it's available now.

Cheers,
deryck

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/code/model/branchjob.py'
2--- lib/lp/code/model/branchjob.py 2010-11-12 00:18:20 +0000
3+++ lib/lp/code/model/branchjob.py 2011-05-11 14:33:21 +0000
4@@ -54,7 +54,6 @@
5 classProvides,
6 implements,
7 )
8-from zope.security.proxy import removeSecurityProxy
9
10 from canonical.config import config
11 from canonical.database.enumcol import EnumCol
12@@ -105,6 +104,7 @@
13 from lp.services.job.interfaces.job import JobStatus
14 from lp.services.job.model.job import Job
15 from lp.services.job.runner import BaseRunnableJob
16+from lp.scripts.helpers import TransactionFreeOperation
17 from lp.translations.interfaces.translationimportqueue import (
18 ITranslationImportQueue,
19 )
20@@ -171,6 +171,7 @@
21 This job scans a branch for new revisions.
22 """)
23
24+
25 class BranchJob(SQLBase):
26 """Base class for jobs related to branches."""
27
28@@ -365,7 +366,7 @@
29 yield
30 server.stop_server()
31
32- def run(self):
33+ def run(self, _check_transaction=False):
34 """See `IBranchUpgradeJob`."""
35 # Set up the new branch structure
36 upgrade_branch_path = tempfile.mkdtemp()
37@@ -376,10 +377,13 @@
38 self.branch.getInternalBzrUrl())
39 source_branch_transport.clone('.bzr').copy_tree_to_transport(
40 upgrade_transport.clone('.bzr'))
41+ transaction.commit()
42 upgrade_branch = BzrBranch.open_from_transport(upgrade_transport)
43
44- # Perform the upgrade.
45- upgrade(upgrade_branch.base)
46+ # No transactions are open so the DB connection won't be killed.
47+ with TransactionFreeOperation():
48+ # Perform the upgrade.
49+ upgrade(upgrade_branch.base)
50
51 # Re-open the branch, since its format has changed.
52 upgrade_branch = BzrBranch.open_from_transport(
53
54=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
55--- lib/lp/code/model/tests/test_branchjob.py 2011-01-20 19:07:26 +0000
56+++ lib/lp/code/model/tests/test_branchjob.py 2011-05-11 14:33:21 +0000
57@@ -75,6 +75,7 @@
58 from lp.code.model.branchrevision import BranchRevision
59 from lp.code.model.revision import RevisionSet
60 from lp.codehosting.vfs import branch_id_to_path
61+from lp.scripts.helpers import TransactionFreeOperation
62 from lp.services.job.interfaces.job import JobStatus
63 from lp.services.job.model.job import Job
64 from lp.services.osutils import override_environ
65@@ -193,7 +194,6 @@
66 """
67 self.useBzrBranches(direct_database=True)
68 branch, tree = self.create_branch_and_tree()
69- first_revision = 'rev-1'
70 tree_transport = tree.bzrdir.root_transport
71 tree_transport.put_bytes("hello.txt", "Hello World\n")
72 tree.add('hello.txt')
73@@ -307,7 +307,8 @@
74
75 job = BranchUpgradeJob.create(db_branch)
76 self.becomeDbUser(config.upgrade_branches.dbuser)
77- job.run()
78+ with TransactionFreeOperation.require():
79+ job.run()
80 new_branch = Branch.open(tree.branch.base)
81 self.assertEqual(
82 new_branch.repository._format.get_format_string(),
83@@ -632,9 +633,9 @@
84 tree.merge_from_branch(tree3.branch, force=True)
85 if include_ghost:
86 tree.add_parent_tree_id('rev2c-id')
87- tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000, timezone=0,
88- committer='J. Random Hacker <jrandom@example.org>',
89- authors=authors)
90+ tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000,
91+ timezone=0, authors=authors,
92+ committer='J. Random Hacker <jrandom@example.org>')
93 return RevisionsAddedJob.create(branch, 'rev2d-id', 'rev2d-id', '')
94
95 def test_getMergedRevisionIDs(self):
96@@ -719,7 +720,7 @@
97
98 def test_getRevisionMessage_with_merge_authors(self):
99 """Merge authors are included after the main bzr log."""
100- person = self.factory.makePerson(name='baz',
101+ self.factory.makePerson(name='baz',
102 displayname='Basil Blaine',
103 email='baz@blaine.com',
104 email_address_status=EmailAddressStatus.VALIDATED)
105@@ -911,15 +912,6 @@
106 self.assertEqual(
107 job.getRevisionMessage(first_revision, 1), expected)
108
109- expected_diff = (
110- "=== modified file 'hello.txt'" '\n'
111- "--- hello.txt" '\t' "2001-09-09 01:46:40 +0000" '\n'
112- "+++ hello.txt" '\t' "2001-09-10 05:33:20 +0000" '\n'
113- "@@ -1,1 +1,3 @@" '\n'
114- " Hello World" '\n'
115- "+" '\n'
116- "+Foo Bar" '\n'
117- '\n')
118 expected_message = (
119 u"-"*60 + '\n'
120 "revno: 2" '\n'
121@@ -1039,7 +1031,6 @@
122 in which case an arbitrary unique string is used.
123 :returns: The revision of this commit.
124 """
125- seen_dirs = set()
126 for file_pair in files:
127 file_name = file_pair[0]
128 try:
129@@ -1182,7 +1173,7 @@
130 # The content of the uploaded file is stored in the librarian.
131 # The uploader of a POT is the series owner.
132 POT_CONTENT = "pot content\n"
133- entries = self._runJobWithFile(
134+ self._runJobWithFile(
135 TranslationsBranchImportMode.IMPORT_TEMPLATES,
136 'foo.pot', POT_CONTENT)
137 # Commit so that the file is stored in the librarian.
138@@ -1307,7 +1298,7 @@
139 self._makeProductSeries(
140 TranslationsBranchImportMode.IMPORT_TEMPLATES)
141 # Put the job in ready state.
142- job = self._makeRosettaUploadJob()
143+ self._makeRosettaUploadJob()
144 ready_jobs = list(RosettaUploadJob.iterReady())
145 self.assertEqual([], ready_jobs)
146
147
148=== modified file 'lib/lp/scripts/helpers.py'
149--- lib/lp/scripts/helpers.py 2010-08-20 20:31:18 +0000
150+++ lib/lp/scripts/helpers.py 2011-05-11 14:33:21 +0000
151@@ -4,8 +4,9 @@
152 """Helpers for command line tools."""
153
154 __metaclass__ = type
155-__all__ = ["LPOptionParser"]
156+__all__ = ["LPOptionParser", "TransactionFreeOperation", ]
157
158+import contextlib
159 from copy import copy
160 from datetime import datetime
161 from optparse import (
162@@ -14,6 +15,8 @@
163 OptionValueError,
164 )
165
166+import transaction
167+
168 from canonical.launchpad.scripts.logger import logger_options
169
170
171@@ -55,8 +58,46 @@
172 Automatically adds our standard --verbose, --quiet options that
173 tie into our logging system.
174 """
175+
176 def __init__(self, *args, **kw):
177 kw.setdefault('option_class', LPOption)
178 OptionParser.__init__(self, *args, **kw)
179 logger_options(self)
180
181+
182+class TransactionFreeOperation:
183+ """Ensure that an operation has no active transactions.
184+
185+ This helps ensure that long-running operations do not hold a database
186+ transaction. Long-running operations that hold a database transaction
187+ may have their database connection killed, and hold locks that interfere
188+ with other updates.
189+ """
190+
191+ count = 0
192+
193+ @staticmethod
194+ def any_active_transactions():
195+ return transaction.manager._txns != {}
196+
197+ @classmethod
198+ def __enter__(cls):
199+ if cls.any_active_transactions():
200+ raise AssertionError('Transaction open before operation!')
201+
202+ @classmethod
203+ def __exit__(cls, exc_type, exc_value, traceback):
204+ if cls.any_active_transactions():
205+ raise AssertionError('Operation opened transaction!')
206+ cls.count += 1
207+
208+ @classmethod
209+ @contextlib.contextmanager
210+ def require(cls):
211+ """Require that TransactionFreeOperation is used at least once."""
212+ old_count = cls.count
213+ try:
214+ yield
215+ finally:
216+ if old_count >= cls.count:
217+ raise AssertionError('TransactionFreeOperation was not used.')
218
219=== added file 'lib/lp/scripts/tests/test_helpers.py'
220--- lib/lp/scripts/tests/test_helpers.py 1970-01-01 00:00:00 +0000
221+++ lib/lp/scripts/tests/test_helpers.py 2011-05-11 14:33:21 +0000
222@@ -0,0 +1,53 @@
223+# Copyright 2011 Canonical Ltd. This software is licensed under the
224+# GNU Affero General Public License version 3 (see the file LICENSE).
225+
226+"""Test the helpers."""
227+
228+__metaclass__ = type
229+
230+from testtools.testcase import ExpectedException
231+import transaction
232+
233+from lp.testing import TestCase
234+from lp.scripts.helpers import TransactionFreeOperation
235+
236+
237+class TestTransactionFreeOperation(TestCase):
238+
239+ def setUp(self):
240+ """We can ignore transactions in general, but this test case cares."""
241+ super(TestTransactionFreeOperation, self).setUp()
242+ transaction.abort()
243+
244+ def test_pending_transaction(self):
245+ """When a transaction is pending before the operation, raise."""
246+ transaction.begin()
247+ with ExpectedException(
248+ AssertionError, 'Transaction open before operation'):
249+ with TransactionFreeOperation:
250+ pass
251+
252+ def test_transaction_during_operation(self):
253+ """When the operation creates a transaction, raise."""
254+ with ExpectedException(
255+ AssertionError, 'Operation opened transaction!'):
256+ with TransactionFreeOperation:
257+ transaction.begin()
258+
259+ def test_transaction_free(self):
260+ """When there are no transactions, do not raise."""
261+ with TransactionFreeOperation:
262+ pass
263+
264+ def test_require_no_TransactionFreeOperation(self):
265+ """If TransactionFreeOperation is not used, raise."""
266+ with ExpectedException(
267+ AssertionError, 'TransactionFreeOperation was not used.'):
268+ with TransactionFreeOperation.require():
269+ pass
270+
271+ def test_require_with_TransactionFreeOperation(self):
272+ """If TransactionFreeOperation is used, do not raise."""
273+ with TransactionFreeOperation.require():
274+ with TransactionFreeOperation:
275+ pass