Merge lp:~cjwatson/launchpad/copy-lock-archive into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/copy-lock-archive
Merge into: lp:launchpad
Diff against target: 136 lines (+57/-3)
3 files modified
lib/lp/services/database/locking.py (+5/-0)
lib/lp/soyuz/model/packagecopyjob.py (+11/-3)
lib/lp/soyuz/tests/test_packagecopyjob.py (+41/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/copy-lock-archive
Reviewer Review Type Date Requested Status
William Grant Needs Information
Review via email: mp+279275@code.launchpad.net

Commit message

Take an advisory lock on the target archive when copying packages.

Description of the change

Take an advisory lock on the target archive when copying packages; otherwise it's possible for multiple copies of the same package into different series in the same archive to race with the conflict checker and create conflicting builds.

The main potential downside that I can think of is that a large mass sync into the Ubuntu archive could potentially result in quite a few copies hitting the lock and needing to be retried repeatedly, taking those jobs over the retry limit. I *think* that the ten-minute delay on retries means that it's unlikely we'll hit the retry limit in practice, but it's possible. I'd welcome advice on whether this is a problem and if so what we might do about it.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

I think this will be a terrible thundering herd problem when bulk copies are performed to a single archive.

review: Needs Information
Revision history for this message
Colin Watson (cjwatson) wrote :

Unmerged revisions

17861. By Colin Watson

Take an advisory lock on the target archive when copying packages.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/services/database/locking.py'
--- lib/lp/services/database/locking.py 2015-03-16 16:22:19 +0000
+++ lib/lp/services/database/locking.py 2015-12-02 13:59:35 +0000
@@ -39,6 +39,11 @@
39 Git repository reference scan.39 Git repository reference scan.
40 """)40 """)
4141
42 PACKAGE_COPY = DBItem(2, """Package copy.
43
44 Package copy.
45 """)
46
4247
43@contextmanager48@contextmanager
44def try_advisory_lock(lock_type, lock_id, store):49def try_advisory_lock(lock_type, lock_id, store):
4550
=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py 2015-07-09 20:06:17 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py 2015-12-02 13:59:35 +0000
@@ -49,6 +49,11 @@
49 IMasterStore,49 IMasterStore,
50 IStore,50 IStore,
51 )51 )
52from lp.services.database.locking import (
53 AdvisoryLockHeld,
54 LockType,
55 try_advisory_lock,
56 )
52from lp.services.database.stormbase import StormBase57from lp.services.database.stormbase import StormBase
53from lp.services.job.interfaces.job import JobStatus58from lp.services.job.interfaces.job import JobStatus
54from lp.services.job.model.job import (59from lp.services.job.model.job import (
@@ -266,7 +271,7 @@
266 user_error_types = (CannotCopy,)271 user_error_types = (CannotCopy,)
267 # Raised when closing bugs ends up hitting another process and272 # Raised when closing bugs ends up hitting another process and
268 # deadlocking.273 # deadlocking.
269 retry_error_types = (TransactionRollbackError,)274 retry_error_types = (TransactionRollbackError, AdvisoryLockHeld)
270 max_retries = 5275 max_retries = 5
271276
272 @classmethod277 @classmethod
@@ -575,7 +580,10 @@
575 def run(self):580 def run(self):
576 """See `IRunnableJob`."""581 """See `IRunnableJob`."""
577 try:582 try:
578 self.attemptCopy()583 with try_advisory_lock(
584 LockType.PACKAGE_COPY, self.target_archive_id,
585 IStore(Archive)):
586 self.attemptCopy()
579 except CannotCopy as e:587 except CannotCopy as e:
580 # Remember the target archive purpose, as otherwise aborting the588 # Remember the target archive purpose, as otherwise aborting the
581 # transaction will forget it.589 # transaction will forget it.
@@ -600,7 +608,7 @@
600 # the job. We will normally have a DistroSeriesDifference608 # the job. We will normally have a DistroSeriesDifference
601 # in this case.609 # in this case.
602 pass610 pass
603 except SuspendJobException:611 except (SuspendJobException, AdvisoryLockHeld):
604 raise612 raise
605 except:613 except:
606 # Abort work done so far, but make sure that we commit the614 # Abort work done so far, but make sure that we commit the
607615
=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py 2015-09-03 15:14:07 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2015-12-02 13:59:35 +0000
@@ -4,8 +4,11 @@
4"""Tests for sync package jobs."""4"""Tests for sync package jobs."""
55
6import operator6import operator
7import os
8import signal
7from textwrap import dedent9from textwrap import dedent
810
11from fixtures import FakeLogger
9from storm.store import Store12from storm.store import Store
10from testtools.content import text_content13from testtools.content import text_content
11from testtools.matchers import (14from testtools.matchers import (
@@ -26,6 +29,10 @@
26 )29 )
27from lp.services.config import config30from lp.services.config import config
28from lp.services.database.interfaces import IStore31from lp.services.database.interfaces import IStore
32from lp.services.database.locking import (
33 LockType,
34 try_advisory_lock,
35 )
29from lp.services.features.testing import FeatureFixture36from lp.services.features.testing import FeatureFixture
30from lp.services.job.interfaces.job import JobStatus37from lp.services.job.interfaces.job import JobStatus
31from lp.services.job.runner import JobRunner38from lp.services.job.runner import JobRunner
@@ -56,6 +63,7 @@
56from lp.soyuz.interfaces.sourcepackageformat import (63from lp.soyuz.interfaces.sourcepackageformat import (
57 ISourcePackageFormatSelectionSet,64 ISourcePackageFormatSelectionSet,
58 )65 )
66from lp.soyuz.model.archive import Archive
59from lp.soyuz.model.packagecopyjob import PackageCopyJob67from lp.soyuz.model.packagecopyjob import PackageCopyJob
60from lp.soyuz.model.queue import PackageUpload68from lp.soyuz.model.queue import PackageUpload
61from lp.soyuz.tests.test_publishing import SoyuzTestPublisher69from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
@@ -326,6 +334,39 @@
326334
327 self.assertRaises(Boom, job.run)335 self.assertRaises(Boom, job.run)
328336
337 def test_run_tries_advisory_lock(self):
338 # A job is retried if an advisory lock for the same archive is held.
339 logger = self.useFixture(FakeLogger())
340 job = create_proper_job(self.factory)
341 switch_dbuser(self.dbuser)
342 # Fork so that we can take an advisory lock from a different
343 # PostgreSQL session.
344 read, write = os.pipe()
345 pid = os.fork()
346 if pid == 0: # child
347 os.close(read)
348 with try_advisory_lock(
349 LockType.PACKAGE_COPY, job.target_archive_id,
350 IStore(Archive)):
351 os.write(write, b"1")
352 try:
353 signal.pause()
354 except KeyboardInterrupt:
355 pass
356 os._exit(0)
357 else: # parent
358 try:
359 os.close(write)
360 os.read(read, 1)
361 runner = JobRunner([job])
362 runner.runAll()
363 self.assertEqual(JobStatus.WAITING, job.status)
364 self.assertEqual([], runner.oops_ids)
365 self.assertIn(
366 "Scheduling retry due to AdvisoryLockHeld", logger.output)
367 finally:
368 os.kill(pid, signal.SIGINT)
369
329 def test_run_posts_copy_failure_as_comment(self):370 def test_run_posts_copy_failure_as_comment(self):
330 # If the job fails with a CannotCopy exception, it swallows the371 # If the job fails with a CannotCopy exception, it swallows the
331 # exception and posts a DistroSeriesDifferenceComment with the372 # exception and posts a DistroSeriesDifferenceComment with the