Merge ~cjwatson/launchpad:branch-scan-race into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 1e8a8e4a940a9906137fba431ad8c810dcc6ff52
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:branch-scan-race
Merge into: launchpad:master
Diff against target: 97 lines (+48/-1)
2 files modified
lib/lp/code/xmlrpc/tests/test_codehosting.py (+47/-1)
requirements/launchpad.txt (+1/-0)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+430166@code.launchpad.net

Commit message

Fix race between branchChanged requests and branch scan jobs

Description of the change

If a `branchChanged` XML-RPC request happened to run at the same time as a branch scan job for the same branch, then they may race to update the same `Branch` row, causing one to have to roll back its transaction and try again. However, a bug in the `transaction` library meant that the exception raised by the before-commit hook to gather job state was incorrectly ignored, and as a result an `InFailedSqlTransaction` exception was raised instead, which isn't retried.

Upgrade to `transaction==3.0.1` to fix this.

Dependencies MP: https://code.launchpad.net/~cjwatson/lp-source-dependencies/+git/lp-source-dependencies/+merge/430165

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/xmlrpc/tests/test_codehosting.py b/lib/lp/code/xmlrpc/tests/test_codehosting.py
2index 60089a2..b25fc57 100644
3--- a/lib/lp/code/xmlrpc/tests/test_codehosting.py
4+++ b/lib/lp/code/xmlrpc/tests/test_codehosting.py
5@@ -5,11 +5,14 @@
6
7 import datetime
8 import os
9+import threading
10
11 import pytz
12 import six
13+import transaction
14 from breezy import controldir
15 from breezy.urlutils import escape
16+from psycopg2.extensions import TransactionRollbackError
17 from testscenarios import WithScenarios, load_tests_apply_scenarios
18 from zope.component import getUtility
19 from zope.security.proxy import removeSecurityProxy
20@@ -19,7 +22,10 @@ from lp.app.errors import NotFoundError
21 from lp.code.bzr import BranchFormat, ControlFormat, RepositoryFormat
22 from lp.code.enums import BranchType
23 from lp.code.errors import UnknownBranchTypeError
24-from lp.code.interfaces.branch import BRANCH_NAME_VALIDATION_ERROR_MESSAGE
25+from lp.code.interfaces.branch import (
26+ BRANCH_NAME_VALIDATION_ERROR_MESSAGE,
27+ IBranchSet,
28+)
29 from lp.code.interfaces.branchlookup import IBranchLookup
30 from lp.code.interfaces.branchtarget import IBranchTarget
31 from lp.code.interfaces.codehosting import (
32@@ -39,6 +45,7 @@ from lp.code.xmlrpc.codehosting import (
33 )
34 from lp.codehosting.inmemory import InMemoryFrontend
35 from lp.services.database.constants import UTC_NOW
36+from lp.services.features.testing import MemoryFeatureFixture
37 from lp.services.scripts.interfaces.scriptactivity import IScriptActivitySet
38 from lp.services.webapp.escaping import html_escape
39 from lp.services.webapp.interfaces import ILaunchBag
40@@ -827,6 +834,45 @@ class CodehostingTest(WithScenarios, TestCaseWithFactory):
41 ),
42 )
43
44+ def test_branchChanged_race_with_scan_job(self):
45+ # The branchChanged XML-RPC request may race with something else
46+ # that's updating the branch at the same time.
47+ if self.layer == FunctionalLayer:
48+ self.skipTest("Only implemented for DB testing.")
49+
50+ self.useFixture(
51+ MemoryFeatureFixture(
52+ {"jobs.celery.enabled_classes": "BranchScanJob"}
53+ )
54+ )
55+ branch = self.factory.makeAnyBranch()
56+ transaction.commit()
57+
58+ def update_branch(unique_name):
59+ login(ANONYMOUS)
60+ branch = getUtility(IBranchSet).getByUniqueName(unique_name)
61+ removeSecurityProxy(branch).last_mirrored_id = "something"
62+ transaction.commit()
63+
64+ unique_name = branch.unique_name # begins transaction
65+ # Force a race by updating the same branch from another thread.
66+ update_thread = threading.Thread(
67+ target=update_branch, args=(unique_name,)
68+ )
69+ update_thread.start()
70+ update_thread.join()
71+
72+ self.codehosting_api.branchChanged(
73+ branch.owner.id,
74+ branch.id,
75+ "",
76+ "rev1",
77+ *self.arbitrary_format_strings,
78+ )
79+ # We get this exception from the failed UPDATE, not
80+ # InFailedSqlTransaction from a subsequent statement.
81+ self.assertRaises(TransactionRollbackError, transaction.commit)
82+
83 def assertNotFound(self, requester, path):
84 """Assert that the given path cannot be found."""
85 if requester not in [LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES]:
86diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
87index bcf7a27..3f92fef 100644
88--- a/requirements/launchpad.txt
89+++ b/requirements/launchpad.txt
90@@ -169,6 +169,7 @@ testresources==0.2.7
91 testscenarios==0.4
92 testtools==2.5.0
93 timeline==0.0.7
94+transaction==3.0.1
95 treq==18.6.0
96 # lp:~launchpad/twisted:lp-backport
97 Twisted==20.3.0+lp9

Subscribers

People subscribed via source and target branches

to status/vote changes: