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
=== modified file 'lib/lp/code/model/branchjob.py'
--- lib/lp/code/model/branchjob.py 2010-11-12 00:18:20 +0000
+++ lib/lp/code/model/branchjob.py 2011-05-11 14:33:21 +0000
@@ -54,7 +54,6 @@
54 classProvides,54 classProvides,
55 implements,55 implements,
56 )56 )
57from zope.security.proxy import removeSecurityProxy
5857
59from canonical.config import config58from canonical.config import config
60from canonical.database.enumcol import EnumCol59from canonical.database.enumcol import EnumCol
@@ -105,6 +104,7 @@
105from lp.services.job.interfaces.job import JobStatus104from lp.services.job.interfaces.job import JobStatus
106from lp.services.job.model.job import Job105from lp.services.job.model.job import Job
107from lp.services.job.runner import BaseRunnableJob106from lp.services.job.runner import BaseRunnableJob
107from lp.scripts.helpers import TransactionFreeOperation
108from lp.translations.interfaces.translationimportqueue import (108from lp.translations.interfaces.translationimportqueue import (
109 ITranslationImportQueue,109 ITranslationImportQueue,
110 )110 )
@@ -171,6 +171,7 @@
171 This job scans a branch for new revisions.171 This job scans a branch for new revisions.
172 """)172 """)
173173
174
174class BranchJob(SQLBase):175class BranchJob(SQLBase):
175 """Base class for jobs related to branches."""176 """Base class for jobs related to branches."""
176177
@@ -365,7 +366,7 @@
365 yield366 yield
366 server.stop_server()367 server.stop_server()
367368
368 def run(self):369 def run(self, _check_transaction=False):
369 """See `IBranchUpgradeJob`."""370 """See `IBranchUpgradeJob`."""
370 # Set up the new branch structure371 # Set up the new branch structure
371 upgrade_branch_path = tempfile.mkdtemp()372 upgrade_branch_path = tempfile.mkdtemp()
@@ -376,10 +377,13 @@
376 self.branch.getInternalBzrUrl())377 self.branch.getInternalBzrUrl())
377 source_branch_transport.clone('.bzr').copy_tree_to_transport(378 source_branch_transport.clone('.bzr').copy_tree_to_transport(
378 upgrade_transport.clone('.bzr'))379 upgrade_transport.clone('.bzr'))
380 transaction.commit()
379 upgrade_branch = BzrBranch.open_from_transport(upgrade_transport)381 upgrade_branch = BzrBranch.open_from_transport(upgrade_transport)
380382
381 # Perform the upgrade.383 # No transactions are open so the DB connection won't be killed.
382 upgrade(upgrade_branch.base)384 with TransactionFreeOperation():
385 # Perform the upgrade.
386 upgrade(upgrade_branch.base)
383387
384 # Re-open the branch, since its format has changed.388 # Re-open the branch, since its format has changed.
385 upgrade_branch = BzrBranch.open_from_transport(389 upgrade_branch = BzrBranch.open_from_transport(
386390
=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
--- lib/lp/code/model/tests/test_branchjob.py 2011-01-20 19:07:26 +0000
+++ lib/lp/code/model/tests/test_branchjob.py 2011-05-11 14:33:21 +0000
@@ -75,6 +75,7 @@
75from lp.code.model.branchrevision import BranchRevision75from lp.code.model.branchrevision import BranchRevision
76from lp.code.model.revision import RevisionSet76from lp.code.model.revision import RevisionSet
77from lp.codehosting.vfs import branch_id_to_path77from lp.codehosting.vfs import branch_id_to_path
78from lp.scripts.helpers import TransactionFreeOperation
78from lp.services.job.interfaces.job import JobStatus79from lp.services.job.interfaces.job import JobStatus
79from lp.services.job.model.job import Job80from lp.services.job.model.job import Job
80from lp.services.osutils import override_environ81from lp.services.osutils import override_environ
@@ -193,7 +194,6 @@
193 """194 """
194 self.useBzrBranches(direct_database=True)195 self.useBzrBranches(direct_database=True)
195 branch, tree = self.create_branch_and_tree()196 branch, tree = self.create_branch_and_tree()
196 first_revision = 'rev-1'
197 tree_transport = tree.bzrdir.root_transport197 tree_transport = tree.bzrdir.root_transport
198 tree_transport.put_bytes("hello.txt", "Hello World\n")198 tree_transport.put_bytes("hello.txt", "Hello World\n")
199 tree.add('hello.txt')199 tree.add('hello.txt')
@@ -307,7 +307,8 @@
307307
308 job = BranchUpgradeJob.create(db_branch)308 job = BranchUpgradeJob.create(db_branch)
309 self.becomeDbUser(config.upgrade_branches.dbuser)309 self.becomeDbUser(config.upgrade_branches.dbuser)
310 job.run()310 with TransactionFreeOperation.require():
311 job.run()
311 new_branch = Branch.open(tree.branch.base)312 new_branch = Branch.open(tree.branch.base)
312 self.assertEqual(313 self.assertEqual(
313 new_branch.repository._format.get_format_string(),314 new_branch.repository._format.get_format_string(),
@@ -632,9 +633,9 @@
632 tree.merge_from_branch(tree3.branch, force=True)633 tree.merge_from_branch(tree3.branch, force=True)
633 if include_ghost:634 if include_ghost:
634 tree.add_parent_tree_id('rev2c-id')635 tree.add_parent_tree_id('rev2c-id')
635 tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000, timezone=0,636 tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000,
636 committer='J. Random Hacker <jrandom@example.org>',637 timezone=0, authors=authors,
637 authors=authors)638 committer='J. Random Hacker <jrandom@example.org>')
638 return RevisionsAddedJob.create(branch, 'rev2d-id', 'rev2d-id', '')639 return RevisionsAddedJob.create(branch, 'rev2d-id', 'rev2d-id', '')
639640
640 def test_getMergedRevisionIDs(self):641 def test_getMergedRevisionIDs(self):
@@ -719,7 +720,7 @@
719720
720 def test_getRevisionMessage_with_merge_authors(self):721 def test_getRevisionMessage_with_merge_authors(self):
721 """Merge authors are included after the main bzr log."""722 """Merge authors are included after the main bzr log."""
722 person = self.factory.makePerson(name='baz',723 self.factory.makePerson(name='baz',
723 displayname='Basil Blaine',724 displayname='Basil Blaine',
724 email='baz@blaine.com',725 email='baz@blaine.com',
725 email_address_status=EmailAddressStatus.VALIDATED)726 email_address_status=EmailAddressStatus.VALIDATED)
@@ -911,15 +912,6 @@
911 self.assertEqual(912 self.assertEqual(
912 job.getRevisionMessage(first_revision, 1), expected)913 job.getRevisionMessage(first_revision, 1), expected)
913914
914 expected_diff = (
915 "=== modified file 'hello.txt'" '\n'
916 "--- hello.txt" '\t' "2001-09-09 01:46:40 +0000" '\n'
917 "+++ hello.txt" '\t' "2001-09-10 05:33:20 +0000" '\n'
918 "@@ -1,1 +1,3 @@" '\n'
919 " Hello World" '\n'
920 "+" '\n'
921 "+Foo Bar" '\n'
922 '\n')
923 expected_message = (915 expected_message = (
924 u"-"*60 + '\n'916 u"-"*60 + '\n'
925 "revno: 2" '\n'917 "revno: 2" '\n'
@@ -1039,7 +1031,6 @@
1039 in which case an arbitrary unique string is used.1031 in which case an arbitrary unique string is used.
1040 :returns: The revision of this commit.1032 :returns: The revision of this commit.
1041 """1033 """
1042 seen_dirs = set()
1043 for file_pair in files:1034 for file_pair in files:
1044 file_name = file_pair[0]1035 file_name = file_pair[0]
1045 try:1036 try:
@@ -1182,7 +1173,7 @@
1182 # The content of the uploaded file is stored in the librarian.1173 # The content of the uploaded file is stored in the librarian.
1183 # The uploader of a POT is the series owner.1174 # The uploader of a POT is the series owner.
1184 POT_CONTENT = "pot content\n"1175 POT_CONTENT = "pot content\n"
1185 entries = self._runJobWithFile(1176 self._runJobWithFile(
1186 TranslationsBranchImportMode.IMPORT_TEMPLATES,1177 TranslationsBranchImportMode.IMPORT_TEMPLATES,
1187 'foo.pot', POT_CONTENT)1178 'foo.pot', POT_CONTENT)
1188 # Commit so that the file is stored in the librarian.1179 # Commit so that the file is stored in the librarian.
@@ -1307,7 +1298,7 @@
1307 self._makeProductSeries(1298 self._makeProductSeries(
1308 TranslationsBranchImportMode.IMPORT_TEMPLATES)1299 TranslationsBranchImportMode.IMPORT_TEMPLATES)
1309 # Put the job in ready state.1300 # Put the job in ready state.
1310 job = self._makeRosettaUploadJob()1301 self._makeRosettaUploadJob()
1311 ready_jobs = list(RosettaUploadJob.iterReady())1302 ready_jobs = list(RosettaUploadJob.iterReady())
1312 self.assertEqual([], ready_jobs)1303 self.assertEqual([], ready_jobs)
13131304
13141305
=== modified file 'lib/lp/scripts/helpers.py'
--- lib/lp/scripts/helpers.py 2010-08-20 20:31:18 +0000
+++ lib/lp/scripts/helpers.py 2011-05-11 14:33:21 +0000
@@ -4,8 +4,9 @@
4"""Helpers for command line tools."""4"""Helpers for command line tools."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = ["LPOptionParser"]7__all__ = ["LPOptionParser", "TransactionFreeOperation", ]
88
9import contextlib
9from copy import copy10from copy import copy
10from datetime import datetime11from datetime import datetime
11from optparse import (12from optparse import (
@@ -14,6 +15,8 @@
14 OptionValueError,15 OptionValueError,
15 )16 )
1617
18import transaction
19
17from canonical.launchpad.scripts.logger import logger_options20from canonical.launchpad.scripts.logger import logger_options
1821
1922
@@ -55,8 +58,46 @@
55 Automatically adds our standard --verbose, --quiet options that58 Automatically adds our standard --verbose, --quiet options that
56 tie into our logging system.59 tie into our logging system.
57 """60 """
61
58 def __init__(self, *args, **kw):62 def __init__(self, *args, **kw):
59 kw.setdefault('option_class', LPOption)63 kw.setdefault('option_class', LPOption)
60 OptionParser.__init__(self, *args, **kw)64 OptionParser.__init__(self, *args, **kw)
61 logger_options(self)65 logger_options(self)
6266
67
68class TransactionFreeOperation:
69 """Ensure that an operation has no active transactions.
70
71 This helps ensure that long-running operations do not hold a database
72 transaction. Long-running operations that hold a database transaction
73 may have their database connection killed, and hold locks that interfere
74 with other updates.
75 """
76
77 count = 0
78
79 @staticmethod
80 def any_active_transactions():
81 return transaction.manager._txns != {}
82
83 @classmethod
84 def __enter__(cls):
85 if cls.any_active_transactions():
86 raise AssertionError('Transaction open before operation!')
87
88 @classmethod
89 def __exit__(cls, exc_type, exc_value, traceback):
90 if cls.any_active_transactions():
91 raise AssertionError('Operation opened transaction!')
92 cls.count += 1
93
94 @classmethod
95 @contextlib.contextmanager
96 def require(cls):
97 """Require that TransactionFreeOperation is used at least once."""
98 old_count = cls.count
99 try:
100 yield
101 finally:
102 if old_count >= cls.count:
103 raise AssertionError('TransactionFreeOperation was not used.')
63104
=== added file 'lib/lp/scripts/tests/test_helpers.py'
--- lib/lp/scripts/tests/test_helpers.py 1970-01-01 00:00:00 +0000
+++ lib/lp/scripts/tests/test_helpers.py 2011-05-11 14:33:21 +0000
@@ -0,0 +1,53 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test the helpers."""
5
6__metaclass__ = type
7
8from testtools.testcase import ExpectedException
9import transaction
10
11from lp.testing import TestCase
12from lp.scripts.helpers import TransactionFreeOperation
13
14
15class TestTransactionFreeOperation(TestCase):
16
17 def setUp(self):
18 """We can ignore transactions in general, but this test case cares."""
19 super(TestTransactionFreeOperation, self).setUp()
20 transaction.abort()
21
22 def test_pending_transaction(self):
23 """When a transaction is pending before the operation, raise."""
24 transaction.begin()
25 with ExpectedException(
26 AssertionError, 'Transaction open before operation'):
27 with TransactionFreeOperation:
28 pass
29
30 def test_transaction_during_operation(self):
31 """When the operation creates a transaction, raise."""
32 with ExpectedException(
33 AssertionError, 'Operation opened transaction!'):
34 with TransactionFreeOperation:
35 transaction.begin()
36
37 def test_transaction_free(self):
38 """When there are no transactions, do not raise."""
39 with TransactionFreeOperation:
40 pass
41
42 def test_require_no_TransactionFreeOperation(self):
43 """If TransactionFreeOperation is not used, raise."""
44 with ExpectedException(
45 AssertionError, 'TransactionFreeOperation was not used.'):
46 with TransactionFreeOperation.require():
47 pass
48
49 def test_require_with_TransactionFreeOperation(self):
50 """If TransactionFreeOperation is used, do not raise."""
51 with TransactionFreeOperation.require():
52 with TransactionFreeOperation:
53 pass