Merge lp:~jtv/launchpad/db-bug-786970 into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 10617
Proposed branch: lp:~jtv/launchpad/db-bug-786970
Merge into: lp:launchpad/db-devel
Diff against target: 559 lines (+211/-115)
10 files modified
database/schema/security.cfg (+6/-0)
lib/lp/registry/browser/distroseries.py (+8/-11)
lib/lp/registry/browser/tests/test_distroseries.py (+7/-3)
lib/lp/registry/interfaces/distroseriesdifference.py (+0/-12)
lib/lp/registry/model/distroseriesdifference.py (+0/-11)
lib/lp/registry/tests/test_distroseriesdifference.py (+0/-18)
lib/lp/soyuz/browser/archive.py (+3/-16)
lib/lp/soyuz/browser/tests/test_package_copying_mixin.py (+0/-25)
lib/lp/soyuz/model/packagecopyjob.py (+62/-0)
lib/lp/soyuz/tests/test_packagecopyjob.py (+125/-19)
To merge this branch: bzr merge lp:~jtv/launchpad/db-bug-786970
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+62292@code.launchpad.net

Commit message

[r=allenap][bug=786970,787462] Register CannotCopy failures as DSDComments.

Description of the change

= Summary =

When a PlainPackageCopyJob fails with a CannotCopy error, that's not really meant to be an oops. It's meant to be an error that the user should address. But how do we tell the user? This job is something you "see going on" in the UI, so we want the errors in the UI as well — email is not an answer.

== Proposed fix ==

Julian and I (with feedback from Colin) decided on the following error-reporting mechanism: any one of the PlainPackageCopyJobs we're dealing with resolves a DistroSeriesDifference, which is what's visible in the UI. And that in turn can have a conversation stream of DistroSeriesDifferenceComments attached. So the job can throw CannotCopy failure messages into that stream, which also solves the problem of logging historical builds.

Next we'll make the page poll, so that these messages can show up on the fly.

== Pre-implementation notes ==

Multiple package copies for multiple DistroSeriesDifferences can go into a single PlainPackageCopyJob. We initially thought we could capture enough information from failures that a CannotCopy can be neatly pegged to a DistroSeriesDifference on the page.

But alas. There are complications: it takes some relatively complicated tracking, and because the checks are to some extent phased, you might see your package failing because some other package broke the whole job. In the end we decided on a simple fix: only handle one package per job. This affects two callsites.

Another question is: what Launchpad user will the error messages be attributed to? An attempt to start a discussion about having a user identity to represent Launchpad in the database for this sort of thing failed miserably. We thought it might be easy impersonate the requesting user (which will be a little disconcerting because automatically generated messages meant primarily for the user will look as if they'd been entered by that same user). But the only current place for that information is Job.requester, which… isn't accessible in Python. In the end Julian gave me the green light to use the Janitor, which fulfills many of the roles that I had wanted a "Launchpad persona" user for.

== Implementation details ==

For once I remembered to test the new code for database privileges. Which revealed a few that were needed, hopefully saving us some agony later on.

Sadly gone are very similar bits of code that Gavin and I developed in parallel for collating packages by archive, so as to bundle them into the minimum number of PlainPackageCopyJobs.

== Tests ==

{{{
./bin/test -vvc lp.soyuz.tests.test_packagecopyjob
}}}

== Demo and Q/A ==

Request asynchronous package copies on a derived distroseries' +localpackagediffs page. (Do this by requesting more copies than the max_synchronous_syncs feature flag setting at once). If any of them fail (and I admit I have yet to figure out how to do that) its error will show up as a comment.

= Launchpad lint =

No lint, but it's complicated. I touched a file that had pre-existing lint, which I did not fix, and then I undid my changes to that file.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

This looks really good.

However, I have a major beef with it: PlainPackageCopyJob is no longer
a plain copy job; it's now heavily intertwined with derivative
distributions. I think PlainPackageCopyJob should be restored, and a
new subclass created, something like DerivativePackageCopyJob, that
knows about DSDs and so forth. It's unfortunate that that involves a
lot of boilerplate code.

Other than that, I think it's a shame that packages can only be copied
one at a time, but I can see this is a far from trivial problem to
solve. It does mean that there's no longer an all-or-nothing guarantee
when copying multiple packages. A *big* downside is that users must
check all DSD comments for errors when they request multiple copies. I
assume both of those are acceptable compromises for now.

[1]

+ try:
+ self.attemptCopy()
+ except CannotCopy, e:
+ self.abort()
+ self.reportFailure(e)
+ self.commit()

Fwiw, the job runner commits after run(), so only the abort() is
needed.

[2]

+ def findMatchingDSDs(self):
[...]
+ source_distro = self.source_archive.distribution
+ return [
+ dsd
+ for dsd in candidates
+ if dsd.parent_series.distribution == source_distro]

Can you do with with IDs instead?

        source_distro_id = self.source_archive.distributionID
        return [
            dsd
            for dsd in candidates
                if dsd.parent_series.distributionID == source_distro_id]

[3]

+ def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
+ # findMatchingDSDs finds matching DSDs for any of the packages
+ # in the job.
+ dsd = self.factory.makeDistroSeriesDifference()
+ job = removeSecurityProxy(self.makeJob(dsd))

This should be naked_job.

Also in test_findMatchingDSDs_ignores_other_source_series.

review: Needs Fixing
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks Gavin.

> However, I have a major beef with it: PlainPackageCopyJob is no longer
> a plain copy job; it's now heavily intertwined with derivative
> distributions. I think PlainPackageCopyJob should be restored, and a
> new subclass created, something like DerivativePackageCopyJob, that
> knows about DSDs and so forth. It's unfortunate that that involves a
> lot of boilerplate code.

That seems like a job for a separate branch though; it will affect a lot more than what's in this branch.

Do I understand that correctly? If so, it seems easier to land this branch first and then make a separate card of splitting up the various usages of PPCJ. Would be nice to get the present branch off my hands!

> Other than that, I think it's a shame that packages can only be copied
> one at a time, but I can see this is a far from trivial problem to
> solve. It does mean that there's no longer an all-or-nothing guarantee
> when copying multiple packages. A *big* downside is that users must
> check all DSD comments for errors when they request multiple copies. I
> assume both of those are acceptable compromises for now.

Yes— we considered that, but it turns out that multi-parent breaks that anyway. You could be selecting DSDs from different parent archives, which have to go into separate jobs regardless!

So in the final analysis we can blame this on the post-last-minute change in requirements.

> + try:
> + self.attemptCopy()
> + except CannotCopy, e:
> + self.abort()
> + self.reportFailure(e)
> + self.commit()
>
> Fwiw, the job runner commits after run(), so only the abort() is
> needed.

Nice one! That commit was there from a time when the exception was still propagated, which I preserved in this branch initially but was then rightly asked to change.

> + def findMatchingDSDs(self):
> [...]
> + source_distro = self.source_archive.distribution
> + return [
> + dsd
> + for dsd in candidates
> + if dsd.parent_series.distribution == source_distro]
>
> Can you do with with IDs instead?

Sure. Silly thing that the ORM needs to fetch the distribution there, really.

> + def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
> + # findMatchingDSDs finds matching DSDs for any of the packages
> + # in the job.
> + dsd = self.factory.makeDistroSeriesDifference()
> + job = removeSecurityProxy(self.makeJob(dsd))
>
> This should be naked_job.
>
> Also in test_findMatchingDSDs_ignores_other_source_series.

Fixed.

Jeroen

Revision history for this message
Gavin Panella (allenap) wrote :

> Thanks Gavin.
>
> > However, I have a major beef with it: PlainPackageCopyJob is no longer
> > a plain copy job; it's now heavily intertwined with derivative
> > distributions. I think PlainPackageCopyJob should be restored, and a
> > new subclass created, something like DerivativePackageCopyJob, that
> > knows about DSDs and so forth. It's unfortunate that that involves a
> > lot of boilerplate code.
>
> That seems like a job for a separate branch though; it will affect a lot more
> than what's in this branch.
>
> Do I understand that correctly? If so, it seems easier to land this branch
> first and then make a separate card of splitting up the various usages of
> PPCJ. Would be nice to get the present branch off my hands!

As agreed otp just now (between bigjools, jtv and allenap), it's cool
to leave this branch as it is and things can be split out again if and
when a solid requirement is there.

>
>
> > Other than that, I think it's a shame that packages can only be copied
> > one at a time, but I can see this is a far from trivial problem to
> > solve. It does mean that there's no longer an all-or-nothing guarantee
> > when copying multiple packages. A *big* downside is that users must
> > check all DSD comments for errors when they request multiple copies. I
> > assume both of those are acceptable compromises for now.
>
> Yes— we considered that, but it turns out that multi-parent breaks that
> anyway. You could be selecting DSDs from different parent archives, which
> have to go into separate jobs regardless!

Of course, that's very true, I didn't think of that.

>
> So in the final analysis we can blame this on the post-last-minute change in
> requirements.

Yes! Grumble, grumble.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2011-05-25 17:29:39 +0000
+++ database/schema/security.cfg 2011-05-28 06:24:35 +0000
@@ -984,6 +984,7 @@
984984
985[sync_packages]985[sync_packages]
986groups=script986groups=script
987public.account = SELECT
987public.archive = SELECT988public.archive = SELECT
988public.archivepermission = SELECT, INSERT989public.archivepermission = SELECT, INSERT
989public.binarypackagebuild = SELECT, INSERT990public.binarypackagebuild = SELECT, INSERT
@@ -1001,12 +1002,17 @@
1001public.distributionsourcepackage = SELECT, INSERT, UPDATE1002public.distributionsourcepackage = SELECT, INSERT, UPDATE
1002public.distroarchseries = SELECT, INSERT1003public.distroarchseries = SELECT, INSERT
1003public.distroseries = SELECT, UPDATE1004public.distroseries = SELECT, UPDATE
1005public.distroseriesdifference = SELECT
1006public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE
1004public.distroseriesparent = SELECT1007public.distroseriesparent = SELECT
1008public.emailaddress = SELECT
1005public.flatpackagesetinclusion = SELECT, INSERT1009public.flatpackagesetinclusion = SELECT, INSERT
1006public.gpgkey = SELECT1010public.gpgkey = SELECT
1007public.job = SELECT, INSERT, UPDATE, DELETE1011public.job = SELECT, INSERT, UPDATE, DELETE
1008public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE1012public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE
1009public.libraryfilecontent = SELECT, INSERT1013public.libraryfilecontent = SELECT, INSERT
1014public.message = SELECT, INSERT, UPDATE
1015public.messagechunk = SELECT, INSERT, UPDATE
1010public.packagebuild = SELECT, INSERT1016public.packagebuild = SELECT, INSERT
1011public.packagecopyjob = SELECT1017public.packagecopyjob = SELECT
1012public.packageset = SELECT, INSERT1018public.packageset = SELECT, INSERT
10131019
=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py 2011-05-27 08:02:37 +0000
+++ lib/lp/registry/browser/distroseries.py 2011-05-28 06:24:35 +0000
@@ -1078,17 +1078,14 @@
1078 """Request sync of packages that can be easily upgraded."""1078 """Request sync of packages that can be easily upgraded."""
1079 target_distroseries = self.context1079 target_distroseries = self.context
1080 target_archive = target_distroseries.main_archive1080 target_archive = target_distroseries.main_archive
1081 differences_by_archive = (1081 job_source = getUtility(IPlainPackageCopyJobSource)
1082 getUtility(IDistroSeriesDifferenceSource)1082
1083 .collateDifferencesByParentArchive(self.getUpgrades()))1083 for dsd in self.getUpgrades():
1084 for source_archive, differences in differences_by_archive.iteritems():1084 job_source.create(
1085 source_package_info = [1085 [specify_dsd_package(dsd)], dsd.parent_series.main_archive,
1086 (difference.source_package_name.name,1086 target_archive, target_distroseries,
1087 difference.parent_source_version)1087 PackagePublishingPocket.UPDATES)
1088 for difference in differences]1088
1089 getUtility(IPlainPackageCopyJobSource).create(
1090 source_package_info, source_archive, target_archive,
1091 target_distroseries, PackagePublishingPocket.UPDATES)
1092 self.request.response.addInfoNotification(1089 self.request.response.addInfoNotification(
1093 (u"Upgrades of {context.displayname} packages have been "1090 (u"Upgrades of {context.displayname} packages have been "
1094 u"requested. Please give Launchpad some time to complete "1091 u"requested. Please give Launchpad some time to complete "
10951092
=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-27 07:50:38 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-28 06:24:35 +0000
@@ -1090,13 +1090,17 @@
1090 with StormStatementRecorder() as recorder1:1090 with StormStatementRecorder() as recorder1:
1091 self.makeView(derived_series).requestUpgrades()1091 self.makeView(derived_series).requestUpgrades()
1092 self.assertThat(recorder1, HasQueryCount(LessThan(10)))1092 self.assertThat(recorder1, HasQueryCount(LessThan(10)))
1093 # The query count does not increase with more differences.1093 # Creating Jobs and DistributionJobs takes 2 extra queries per
1094 for index in xrange(3):1094 # requested sync.
1095 requested_syncs = 3
1096 for index in xrange(requested_syncs):
1095 self.makePackageUpgrade(derived_series=derived_series)1097 self.makePackageUpgrade(derived_series=derived_series)
1096 flush_database_caches()1098 flush_database_caches()
1097 with StormStatementRecorder() as recorder2:1099 with StormStatementRecorder() as recorder2:
1098 self.makeView(derived_series).requestUpgrades()1100 self.makeView(derived_series).requestUpgrades()
1099 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))1101 self.assertThat(
1102 recorder2,
1103 HasQueryCount(Equals(recorder1.count + 2 * requested_syncs)))
11001104
11011105
1102class TestDistroSeriesLocalDifferencesFunctional(TestCaseWithFactory,1106class TestDistroSeriesLocalDifferencesFunctional(TestCaseWithFactory,
11031107
=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
--- lib/lp/registry/interfaces/distroseriesdifference.py 2011-05-24 15:36:35 +0000
+++ lib/lp/registry/interfaces/distroseriesdifference.py 2011-05-28 06:24:35 +0000
@@ -338,15 +338,3 @@
338338
339 Blacklisted items are excluded.339 Blacklisted items are excluded.
340 """340 """
341
342 def collateDifferencesByParentArchive(differences):
343 """Collate the given differences by parent archive.
344
345 The given `IDistroSeriesDifference`s are returned in a `dict`, with
346 the parent `Archive` as keys.
347
348 :param differences: An iterable sequence of `IDistroSeriesDifference`.
349
350 :return: A `dict` of iterable sequences of `IDistroSeriesDifference`
351 keyed by their parent `IArchive`.
352 """
353341
=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py 2011-05-24 11:32:34 +0000
+++ lib/lp/registry/model/distroseriesdifference.py 2011-05-28 06:24:35 +0000
@@ -494,17 +494,6 @@
494 DistroSeriesDifference.source_package_name_id)494 DistroSeriesDifference.source_package_name_id)
495 return DecoratedResultSet(differences, itemgetter(0))495 return DecoratedResultSet(differences, itemgetter(0))
496496
497 @staticmethod
498 def collateDifferencesByParentArchive(differences):
499 by_archive = dict()
500 for difference in differences:
501 archive = difference.parent_series.main_archive
502 if archive in by_archive:
503 by_archive[archive].append(difference)
504 else:
505 by_archive[archive] = [difference]
506 return by_archive
507
508 @cachedproperty497 @cachedproperty
509 def source_pub(self):498 def source_pub(self):
510 """See `IDistroSeriesDifference`."""499 """See `IDistroSeriesDifference`."""
511500
=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py 2011-05-24 15:36:35 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py 2011-05-28 06:24:35 +0000
@@ -1129,24 +1129,6 @@
1129 self.assertContentEqual(1129 self.assertContentEqual(
1130 [], dsd_source.getSimpleUpgrades(dsd.derived_series))1130 [], dsd_source.getSimpleUpgrades(dsd.derived_series))
11311131
1132 def test_collateDifferencesByParentArchive(self):
1133 dsp1 = self.factory.makeDistroSeriesParent()
1134 dsp2 = self.factory.makeDistroSeriesParent()
1135 differences = [
1136 self.factory.makeDistroSeriesDifference(dsp1.derived_series),
1137 self.factory.makeDistroSeriesDifference(dsp2.derived_series),
1138 self.factory.makeDistroSeriesDifference(dsp1.derived_series),
1139 self.factory.makeDistroSeriesDifference(dsp2.derived_series),
1140 ]
1141 dsd_source = getUtility(IDistroSeriesDifferenceSource)
1142 observed = (
1143 dsd_source.collateDifferencesByParentArchive(differences))
1144 expected = {
1145 dsp1.parent_series.main_archive: differences[0::2],
1146 dsp2.parent_series.main_archive: differences[1::2],
1147 }
1148 self.assertEqual(observed, expected)
1149
11501132
1151class TestMostRecentComments(TestCaseWithFactory):1133class TestMostRecentComments(TestCaseWithFactory):
11521134
11531135
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2011-05-26 15:06:44 +0000
+++ lib/lp/soyuz/browser/archive.py 2011-05-28 06:24:35 +0000
@@ -1285,19 +1285,6 @@
1285 dest_display_name)1285 dest_display_name)
12861286
12871287
1288def partition_pubs_by_archive(source_pubs):
1289 """Group `source_pubs` by archive.
1290
1291 :param source_pubs: A sequence of `SourcePackagePublishingHistory`.
1292 :return: A dict mapping `Archive`s to the list of entries from
1293 `source_pubs` that are in that archive.
1294 """
1295 by_source_archive = {}
1296 for spph in source_pubs:
1297 by_source_archive.setdefault(spph.archive, []).append(spph)
1298 return by_source_archive
1299
1300
1301def name_pubs_with_versions(source_pubs):1288def name_pubs_with_versions(source_pubs):
1302 """Annotate each entry from `source_pubs` with its version.1289 """Annotate each entry from `source_pubs` with its version.
13031290
@@ -1328,11 +1315,11 @@
1328 person, dest_archive, dest_series, dest_pocket, spns)1315 person, dest_archive, dest_series, dest_pocket, spns)
13291316
1330 job_source = getUtility(IPlainPackageCopyJobSource)1317 job_source = getUtility(IPlainPackageCopyJobSource)
1331 archive_pubs = partition_pubs_by_archive(source_pubs)1318 for spph in source_pubs:
1332 for source_archive, spphs in archive_pubs.iteritems():
1333 job_source.create(1319 job_source.create(
1334 name_pubs_with_versions(spphs), source_archive, dest_archive,1320 name_pubs_with_versions([spph]), spph.archive, dest_archive,
1335 dest_series, dest_pocket, include_binaries=include_binaries)1321 dest_series, dest_pocket, include_binaries=include_binaries)
1322
1336 return structured("""1323 return structured("""
1337 <p>Requested sync of %s packages.</p>1324 <p>Requested sync of %s packages.</p>
1338 <p>Please allow some time for these to be processed.</p>1325 <p>Please allow some time for these to be processed.</p>
13391326
=== modified file 'lib/lp/soyuz/browser/tests/test_package_copying_mixin.py'
--- lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-05-20 20:40:19 +0000
+++ lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-05-28 06:24:35 +0000
@@ -19,7 +19,6 @@
19 FEATURE_FLAG_MAX_SYNCHRONOUS_SYNCS,19 FEATURE_FLAG_MAX_SYNCHRONOUS_SYNCS,
20 name_pubs_with_versions,20 name_pubs_with_versions,
21 PackageCopyingMixin,21 PackageCopyingMixin,
22 partition_pubs_by_archive,
23 render_cannotcopy_as_html,22 render_cannotcopy_as_html,
24 )23 )
25from lp.soyuz.interfaces.archive import CannotCopy24from lp.soyuz.interfaces.archive import CannotCopy
@@ -104,30 +103,6 @@
104 packages = [self.getUniqueString() for counter in range(300)]103 packages = [self.getUniqueString() for counter in range(300)]
105 self.assertFalse(PackageCopyingMixin().canCopySynchronously(packages))104 self.assertFalse(PackageCopyingMixin().canCopySynchronously(packages))
106105
107 def test_partition_pubs_by_archive_maps_archives_to_pubs(self):
108 # partition_pubs_by_archive returns a dict mapping each archive
109 # to a list of SPPHs on that archive.
110 spph = FakeSPPH()
111 self.assertEqual(
112 {spph.archive: [spph]}, partition_pubs_by_archive([spph]))
113
114 def test_partition_pubs_by_archive_splits_by_archive(self):
115 # partition_pubs_by_archive keeps SPPHs for different archives
116 # separate.
117 spphs = [FakeSPPH() for counter in xrange(2)]
118 mapping = partition_pubs_by_archive(spphs)
119 self.assertEqual(
120 dict((spph.archive, [spph]) for spph in spphs), mapping)
121
122 def test_partition_pubs_by_archive_clusters_by_archive(self):
123 # partition_pubs_by_archive bundles SPPHs for the same archive
124 # into a single dict entry.
125 archive = FakeArchive()
126 spphs = [FakeSPPH(archive=archive) for counter in xrange(2)]
127 mapping = partition_pubs_by_archive(spphs)
128 self.assertEqual([archive], mapping.keys())
129 self.assertContentEqual(spphs, mapping[archive])
130
131 def test_name_pubs_with_versions_lists_packages_and_versions(self):106 def test_name_pubs_with_versions_lists_packages_and_versions(self):
132 # name_pubs_with_versions returns a list of tuples of source107 # name_pubs_with_versions returns a list of tuples of source
133 # package name and source package version, one per SPPH.108 # package name and source package version, one per SPPH.
134109
=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py 2011-05-19 17:20:02 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py 2011-05-28 06:24:35 +0000
@@ -17,6 +17,8 @@
17 Reference,17 Reference,
18 Unicode,18 Unicode,
19 )19 )
20import transaction
21from zope.component import getUtility
20from zope.interface import (22from zope.interface import (
21 classProvides,23 classProvides,
22 implements,24 implements,
@@ -26,13 +28,20 @@
26from canonical.launchpad.components.decoratedresultset import (28from canonical.launchpad.components.decoratedresultset import (
27 DecoratedResultSet,29 DecoratedResultSet,
28 )30 )
31from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
29from canonical.launchpad.interfaces.lpstorm import (32from canonical.launchpad.interfaces.lpstorm import (
30 IMasterStore,33 IMasterStore,
31 IStore,34 IStore,
32 )35 )
33from lp.app.errors import NotFoundError36from lp.app.errors import NotFoundError
37from lp.registry.interfaces.distroseriesdifference import (
38 IDistroSeriesDifferenceSource,
39 )
34from lp.registry.interfaces.pocket import PackagePublishingPocket40from lp.registry.interfaces.pocket import PackagePublishingPocket
35from lp.registry.model.distroseries import DistroSeries41from lp.registry.model.distroseries import DistroSeries
42from lp.registry.interfaces.distroseriesdifferencecomment import (
43 IDistroSeriesDifferenceCommentSource,
44 )
36from lp.services.database.stormbase import StormBase45from lp.services.database.stormbase import StormBase
37from lp.services.job.interfaces.job import JobStatus46from lp.services.job.interfaces.job import JobStatus
38from lp.services.job.model.job import Job47from lp.services.job.model.job import Job
@@ -153,6 +162,11 @@
153162
154class PlainPackageCopyJob(PackageCopyJobDerived):163class PlainPackageCopyJob(PackageCopyJobDerived):
155 """Job that copies packages between archives."""164 """Job that copies packages between archives."""
165 # This job type serves in different places: it supports copying
166 # packages between archives, but also the syncing of packages from
167 # parents into a derived distroseries. We may split these into
168 # separate types at some point, but for now we (allenap, bigjools,
169 # jtv) chose to keep it as one.
156170
157 implements(IPlainPackageCopyJob)171 implements(IPlainPackageCopyJob)
158172
@@ -232,6 +246,18 @@
232246
233 def run(self):247 def run(self):
234 """See `IRunnableJob`."""248 """See `IRunnableJob`."""
249 try:
250 self.attemptCopy()
251 except CannotCopy, e:
252 self.abort()
253 self.reportFailure(e)
254
255 def attemptCopy(self):
256 """Attempt to perform the copy.
257
258 :raise CannotCopy: If the copy fails for a reason that the user
259 can deal with.
260 """
235 if self.target_archive.is_ppa:261 if self.target_archive.is_ppa:
236 if self.target_pocket != PackagePublishingPocket.RELEASE:262 if self.target_pocket != PackagePublishingPocket.RELEASE:
237 raise CannotCopy(263 raise CannotCopy(
@@ -250,6 +276,42 @@
250 series=self.target_distroseries, pocket=self.target_pocket,276 series=self.target_distroseries, pocket=self.target_pocket,
251 include_binaries=self.include_binaries, check_permissions=False)277 include_binaries=self.include_binaries, check_permissions=False)
252278
279 def abort(self):
280 """Abort work."""
281 transaction.abort()
282
283 def findMatchingDSDs(self):
284 """Find any `DistroSeriesDifference`s that this job might resolve."""
285 dsd_source = getUtility(IDistroSeriesDifferenceSource)
286 target_series = self.target_distroseries
287 candidates = dsd_source.getForDistroSeries(
288 distro_series=target_series)
289 # The job doesn't know what distroseries a given package is
290 # coming from, and the version number in the DSD may have
291 # changed. We can however filter out DSDs that are from
292 # different distributions, based on the job's target archive.
293 source_distro_id = self.source_archive.distributionID
294 return [
295 dsd
296 for dsd in candidates
297 if dsd.parent_series.distributionID == source_distro_id]
298
299 def reportFailure(self, cannotcopy_exception):
300 """Attempt to report failure to the user."""
301 message = unicode(cannotcopy_exception)
302 dsds = self.findMatchingDSDs()
303 comment_source = getUtility(IDistroSeriesDifferenceCommentSource)
304
305 # Register the error comment in the name of the Janitor. Not a
306 # great choice, but we have no user identity to represent
307 # Launchpad; it's far too costly to create one; and
308 # impersonating the requester can be misleading and would also
309 # involve extra bookkeeping.
310 reporting_persona = getUtility(ILaunchpadCelebrities).janitor
311
312 for dsd in dsds:
313 comment_source.new(dsd, reporting_persona, message)
314
253 def __repr__(self):315 def __repr__(self):
254 """Returns an informative representation of the job."""316 """Returns an informative representation of the job."""
255 parts = ["%s to copy" % self.__class__.__name__]317 parts = ["%s to copy" % self.__class__.__name__]
256318
=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-24 14:29:51 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-28 06:24:35 +0000
@@ -8,7 +8,12 @@
8from zope.component import getUtility8from zope.component import getUtility
9from zope.security.proxy import removeSecurityProxy9from zope.security.proxy import removeSecurityProxy
1010
11from canonical.config import config
12from canonical.launchpad.interfaces.lpstorm import IStore
11from canonical.testing import LaunchpadZopelessLayer13from canonical.testing import LaunchpadZopelessLayer
14from lp.registry.model.distroseriesdifferencecomment import (
15 DistroSeriesDifferenceComment,
16 )
12from lp.registry.interfaces.pocket import PackagePublishingPocket17from lp.registry.interfaces.pocket import PackagePublishingPocket
13from lp.services.features.testing import FeatureFixture18from lp.services.features.testing import FeatureFixture
14from lp.services.job.interfaces.job import JobStatus19from lp.services.job.interfaces.job import JobStatus
@@ -34,15 +39,23 @@
34 run_script,39 run_script,
35 TestCaseWithFactory,40 TestCaseWithFactory,
36 )41 )
3742from lp.testing.fakemethod import FakeMethod
3843
39class PlainPackageCopyJobTests(TestCaseWithFactory):44
40 """Test case for PlainPackageCopyJob."""45def get_dsd_comments(dsd):
4146 """Retrieve `DistroSeriesDifferenceComment`s for `dsd`."""
42 layer = LaunchpadZopelessLayer47 return IStore(dsd).find(
4348 DistroSeriesDifferenceComment,
44 def makeJob(self, dsd):49 DistroSeriesDifferenceComment.distro_series_difference == dsd)
50
51
52class LocalTestHelper:
53 """Put test helpers that want to be in the test classes here."""
54
55 def makeJob(self, dsd=None):
45 """Create a `PlainPackageCopyJob` that would resolve `dsd`."""56 """Create a `PlainPackageCopyJob` that would resolve `dsd`."""
57 if dsd is None:
58 dsd = self.factory.makeDistroSeriesDifference()
46 source_packages = [specify_dsd_package(dsd)]59 source_packages = [specify_dsd_package(dsd)]
47 source_archive = dsd.parent_series.main_archive60 source_archive = dsd.parent_series.main_archive
48 target_archive = dsd.derived_series.main_archive61 target_archive = dsd.derived_series.main_archive
@@ -58,6 +71,12 @@
58 self.layer.switchDbUser('sync_packages')71 self.layer.switchDbUser('sync_packages')
59 job.run()72 job.run()
6073
74
75class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper):
76 """Test case for PlainPackageCopyJob."""
77
78 layer = LaunchpadZopelessLayer
79
61 def test_create(self):80 def test_create(self):
62 # A PackageCopyJob can be created and stores its arguments.81 # A PackageCopyJob can be created and stores its arguments.
63 distroseries = self.factory.makeDistroSeries()82 distroseries = self.factory.makeDistroSeries()
@@ -105,23 +124,59 @@
105124
106 def test_getActiveJobs_only_returns_waiting_jobs(self):125 def test_getActiveJobs_only_returns_waiting_jobs(self):
107 # getActiveJobs ignores jobs that aren't in the WAITING state.126 # getActiveJobs ignores jobs that aren't in the WAITING state.
108 job = self.makeJob(self.factory.makeDistroSeriesDifference())127 job = self.makeJob()
109 removeSecurityProxy(job).job._status = JobStatus.RUNNING128 removeSecurityProxy(job).job._status = JobStatus.RUNNING
110 source = getUtility(IPlainPackageCopyJobSource)129 source = getUtility(IPlainPackageCopyJobSource)
111 self.assertContentEqual([], source.getActiveJobs(job.target_archive))130 self.assertContentEqual([], source.getActiveJobs(job.target_archive))
112131
113 def test_run_unknown_package(self):132 def test_run_raises_errors(self):
114 # A job properly records failure.133 # A job reports unexpected errors as exceptions.
134 class Boom(Exception):
135 pass
136
137 job = self.makeJob()
138 removeSecurityProxy(job).attemptCopy = FakeMethod(failure=Boom())
139
140 self.assertRaises(Boom, job.run)
141
142 def test_run_posts_copy_failure_as_comment(self):
143 # If the job fails with a CannotCopy exception, it swallows the
144 # exception and posts a DistroSeriesDifferenceComment with the
145 # failure message.
146 dsd = self.factory.makeDistroSeriesDifference()
147 self.factory.makeArchive(distribution=dsd.derived_series.distribution)
148 job = self.makeJob(dsd)
149 removeSecurityProxy(job).attemptCopy = FakeMethod(
150 failure=CannotCopy("Server meltdown"))
151
152 # The job's error handling will abort, so commit the objects we
153 # created as would have happened in real life.
154 transaction.commit()
155
156 job.run()
157
158 self.assertEqual(
159 ["Server meltdown"],
160 [comment.body_text for comment in get_dsd_comments(dsd)])
161
162 def test_run_cannot_copy_unknown_package(self):
163 # Attempting to copy an unknown package is reported as a
164 # failure.
115 distroseries = self.factory.makeDistroSeries()165 distroseries = self.factory.makeDistroSeries()
116 archive1 = self.factory.makeArchive(distroseries.distribution)166 archive1 = self.factory.makeArchive(distroseries.distribution)
117 archive2 = self.factory.makeArchive(distroseries.distribution)167 archive2 = self.factory.makeArchive(distroseries.distribution)
118 source = getUtility(IPlainPackageCopyJobSource)168 job_source = getUtility(IPlainPackageCopyJobSource)
119 job = source.create(169 job = job_source.create(
120 source_packages=[("foo", "1.0-1")], source_archive=archive1,170 source_packages=[("foo", "1.0-1")], source_archive=archive1,
121 target_archive=archive2, target_distroseries=distroseries,171 target_archive=archive2, target_distroseries=distroseries,
122 target_pocket=PackagePublishingPocket.RELEASE,172 target_pocket=PackagePublishingPocket.RELEASE,
123 include_binaries=False)173 include_binaries=False)
124 self.assertRaises(CannotCopy, self.runJob, job)174 naked_job = removeSecurityProxy(job)
175 naked_job.reportFailure = FakeMethod()
176
177 job.run()
178
179 self.assertEqual(1, naked_job.reportFailure.call_count)
125180
126 def test_target_ppa_non_release_pocket(self):181 def test_target_ppa_non_release_pocket(self):
127 # When copying to a PPA archive the target must be the release pocket.182 # When copying to a PPA archive the target must be the release pocket.
@@ -134,7 +189,13 @@
134 target_archive=archive2, target_distroseries=distroseries,189 target_archive=archive2, target_distroseries=distroseries,
135 target_pocket=PackagePublishingPocket.UPDATES,190 target_pocket=PackagePublishingPocket.UPDATES,
136 include_binaries=False)191 include_binaries=False)
137 self.assertRaises(CannotCopy, self.runJob, job)192
193 naked_job = removeSecurityProxy(job)
194 naked_job.reportFailure = FakeMethod()
195
196 job.run()
197
198 self.assertEqual(1, naked_job.reportFailure.call_count)
138199
139 def test_run(self):200 def test_run(self):
140 # A proper test run synchronizes packages.201 # A proper test run synchronizes packages.
@@ -164,8 +225,9 @@
164225
165 source = getUtility(IPlainPackageCopyJobSource)226 source = getUtility(IPlainPackageCopyJobSource)
166 job = source.create(227 job = source.create(
167 source_packages=[("libc", "2.8-1")], source_archive=breezy_archive,228 source_packages=[("libc", "2.8-1")],
168 target_archive=target_archive, target_distroseries=target_series,229 source_archive=breezy_archive, target_archive=target_archive,
230 target_distroseries=target_series,
169 target_pocket=PackagePublishingPocket.RELEASE,231 target_pocket=PackagePublishingPocket.RELEASE,
170 include_binaries=False)232 include_binaries=False)
171 self.assertContentEqual(233 self.assertContentEqual(
@@ -275,8 +337,7 @@
275 def test_getPendingJobsPerPackage_ignores_other_distroseries(self):337 def test_getPendingJobsPerPackage_ignores_other_distroseries(self):
276 # getPendingJobsPerPackage only looks for jobs on the indicated338 # getPendingJobsPerPackage only looks for jobs on the indicated
277 # distroseries.339 # distroseries.
278 dsd = self.factory.makeDistroSeriesDifference()340 self.makeJob()
279 self.makeJob(dsd)
280 other_series = self.factory.makeDistroSeries()341 other_series = self.factory.makeDistroSeries()
281 job_source = getUtility(IPlainPackageCopyJobSource)342 job_source = getUtility(IPlainPackageCopyJobSource)
282 self.assertEqual(343 self.assertEqual(
@@ -333,3 +394,48 @@
333 job_source = getUtility(IPlainPackageCopyJobSource)394 job_source = getUtility(IPlainPackageCopyJobSource)
334 self.assertEqual(395 self.assertEqual(
335 {}, job_source.getPendingJobsPerPackage(dsd.derived_series))396 {}, job_source.getPendingJobsPerPackage(dsd.derived_series))
397
398 def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
399 # findMatchingDSDs finds matching DSDs for any of the packages
400 # in the job.
401 dsd = self.factory.makeDistroSeriesDifference()
402 naked_job = removeSecurityProxy(self.makeJob(dsd))
403 self.assertContentEqual([dsd], naked_job.findMatchingDSDs())
404
405 def test_findMatchingDSDs_ignores_other_source_series(self):
406 # findMatchingDSDs tries to ignore DSDs that are for different
407 # parent series than the job's source series. (This can't be
408 # done with perfect precision because the job doesn't keep track
409 # of source distroseries, but in practice it should be good
410 # enough).
411 dsd = self.factory.makeDistroSeriesDifference()
412 naked_job = removeSecurityProxy(self.makeJob(dsd))
413
414 # If the dsd differs only in parent series, that's enough to
415 # make it a non-match.
416 removeSecurityProxy(dsd).parent_series = (
417 self.factory.makeDistroSeries())
418
419 self.assertContentEqual([], naked_job.findMatchingDSDs())
420
421
422class TestPlainPackageCopyJobPrivileges(TestCaseWithFactory, LocalTestHelper):
423 """Test that `PlainPackageCopyJob` has the privileges it needs.
424
425 This test looks for errors, not failures. It's here only to see that
426 these operations don't run into any privilege limitations.
427 """
428
429 layer = LaunchpadZopelessLayer
430
431 def test_findMatchingDSDs(self):
432 job = self.makeJob()
433 transaction.commit()
434 self.layer.switchDbUser(config.IPlainPackageCopyJobSource.dbuser)
435 removeSecurityProxy(job).findMatchingDSDs()
436
437 def test_reportFailure(self):
438 job = self.makeJob()
439 transaction.commit()
440 self.layer.switchDbUser(config.IPlainPackageCopyJobSource.dbuser)
441 removeSecurityProxy(job).reportFailure(CannotCopy("Mommy it hurts"))

Subscribers

People subscribed via source and target branches

to status/vote changes: