Merge lp:~cjwatson/launchpad/garbo-archivepermission-duplicates into lp:launchpad

Proposed by Colin Watson on 2012-07-18
Status: Merged
Approved by: Colin Watson on 2012-07-18
Approved revision: no longer in the source branch.
Merged at revision: 15649
Proposed branch: lp:~cjwatson/launchpad/garbo-archivepermission-duplicates
Merge into: lp:launchpad
Diff against target: 152 lines (+99/-0)
3 files modified
database/schema/security.cfg (+1/-0)
lib/lp/scripts/garbo.py (+19/-0)
lib/lp/scripts/tests/test_garbo.py (+79/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/garbo-archivepermission-duplicates
Reviewer Review Type Date Requested Status
Raphaël Badin (community) 2012-07-18 Approve on 2012-07-18
Francesco Banconi (community) code* 2012-07-18 Approve on 2012-07-18
Review via email: mp+115554@code.launchpad.net

Commit Message

Remove duplicate rows from ArchivePermission, created due to bug 887185.

Description of the Change

== Summary ==

There are some duplicated rows in ArchivePermission left over from an old (fixed) bug. These get in the way because Archive.removePackagesetUploader uses .one().

== Proposed fix ==

Add a BulkPruner-based garbo job to get rid of the duplicates. Previous analysis showed that the only problems have non-NULL packageset and duplicate person, archive, packageset, permission, so we'll get rid of all but the first id for all such groups.

== LOC Rationale ==

+99, but it will all be removed again once the job has completed.

== Tests ==

bin/test -vvct test_DuplicateArchivePermissionPruner

== Demo and Q/A ==

dogfood's database is already in an appropriately broken state. Dump the archivepermission table, run the garbo job, and dump it again, and then verify that all the removed IDs (there should be 190, I believe) correspond to duplicates and that exactly one of each duplicated group remains.

To post a comment you must log in.
Francesco Banconi (frankban) wrote :

Thanks Colin, this branch looks good.
It seems the tests contain some repeated code (to create ArchivePermissions), but I think that's ok since the job will be removed.
Tests pass, and you said on IRC that you checked the SQL fragment by hand on dogfood.

review: Approve (code*)
Raphaël Badin (rvb) wrote :

Thanks for the cleanup!

The test code could probably be more compact, but, like Francesco noted, this doesn't matter much since this will be ditched eventually.

I didn't know about window functions, I've always used regular aggregate functions to do that kind of stuff. Thanks Colin!

review: Approve
Colin Watson (cjwatson) wrote :

Right. Some of the repeated code was necessary to deliberately avoid
guards against duplication in the ArchivePermission model; the rest was
just so that the tests were more or less regular.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2012-07-17 06:21:47 +0000
3+++ database/schema/security.cfg 2012-07-18 13:53:21 +0000
4@@ -2201,6 +2201,7 @@
5 [garbo]
6 groups=script,read
7 public.account = SELECT, DELETE
8+public.archivepermission = SELECT, DELETE
9 public.answercontact = SELECT, DELETE
10 public.branch = SELECT, UPDATE
11 public.branchjob = SELECT, DELETE
12
13=== modified file 'lib/lp/scripts/garbo.py'
14--- lib/lp/scripts/garbo.py 2012-07-10 06:57:47 +0000
15+++ lib/lp/scripts/garbo.py 2012-07-18 13:53:21 +0000
16@@ -93,6 +93,7 @@
17 MAIN_STORE,
18 MASTER_FLAVOR,
19 )
20+from lp.soyuz.model.archivepermission import ArchivePermission
21 from lp.translations.interfaces.potemplate import IPOTemplateSet
22 from lp.translations.model.potmsgset import POTMsgSet
23 from lp.translations.model.potranslation import POTranslation
24@@ -990,6 +991,23 @@
25 transaction.commit()
26
27
28+class DuplicateArchivePermissionPruner(BulkPruner):
29+ """Cleans up duplicate ArchivePermission rows created by bug 887185."""
30+ target_table_class = ArchivePermission
31+ ids_to_prune_query = """
32+ SELECT id FROM (
33+ SELECT id, rank() OVER w AS rank
34+ FROM ArchivePermission
35+ WHERE packageset IS NOT NULL
36+ WINDOW w AS (
37+ PARTITION BY person, archive, packageset, permission
38+ ORDER BY id
39+ )
40+ ) AS whatever
41+ WHERE rank > 1
42+ """
43+
44+
45 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
46 """Abstract base class to run a collection of TunableLoops."""
47 script_name = None # Script name for locking and database user. Override.
48@@ -1267,6 +1285,7 @@
49 BugWatchActivityPruner,
50 CodeImportEventPruner,
51 CodeImportResultPruner,
52+ DuplicateArchivePermissionPruner,
53 HWSubmissionEmailLinker,
54 LoginTokenPruner,
55 ObsoleteBugAttachmentPruner,
56
57=== modified file 'lib/lp/scripts/tests/test_garbo.py'
58--- lib/lp/scripts/tests/test_garbo.py 2012-07-10 07:08:54 +0000
59+++ lib/lp/scripts/tests/test_garbo.py 2012-07-18 13:53:21 +0000
60@@ -97,6 +97,9 @@
61 MASTER_FLAVOR,
62 )
63 from lp.services.worlddata.interfaces.language import ILanguageSet
64+from lp.soyuz.enums import ArchivePermissionType
65+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
66+from lp.soyuz.model.archivepermission import ArchivePermission
67 from lp.testing import (
68 person_logged_in,
69 TestCase,
70@@ -1016,6 +1019,82 @@
71 self.runHourly()
72 self.assertNotEqual(old_update, naked_bug.heat_last_updated)
73
74+ def test_DuplicateArchivePermissionPruner_removes_duplicate_rows(self):
75+ # DuplicateArchivePermissionPruner removes duplicated packageset
76+ # permissions.
77+ switch_dbuser('testadmin')
78+ archive = self.factory.makeArchive()
79+ person = self.factory.makePerson()
80+ packageset = self.factory.makePackageset()
81+ for _ in range(3):
82+ ArchivePermission(
83+ archive=archive, person=person, packageset=packageset,
84+ permission=ArchivePermissionType.UPLOAD)
85+ ap_set = getUtility(IArchivePermissionSet)
86+ self.assertEqual(
87+ 3, ap_set.uploadersForPackageset(archive, packageset).count())
88+ self.runDaily()
89+ self.assertEqual(
90+ 1, ap_set.uploadersForPackageset(archive, packageset).count())
91+
92+ def test_DuplicateArchivePermissionPruner_skips_unique_rows(self):
93+ # DuplicateArchivePermissionPruner leaves unique packageset
94+ # permissions alone.
95+ switch_dbuser('testadmin')
96+ archive_one = self.factory.makeArchive()
97+ archive_two = self.factory.makeArchive()
98+ person_one = self.factory.makePerson()
99+ person_two = self.factory.makePerson()
100+ packageset_one = self.factory.makePackageset()
101+ packageset_two = self.factory.makePackageset()
102+ for archive, person, packageset in (
103+ (archive_one, person_one, packageset_one),
104+ (archive_two, person_one, packageset_one),
105+ (archive_one, person_two, packageset_one),
106+ (archive_one, person_one, packageset_two),
107+ ):
108+ ArchivePermission(
109+ archive=archive, person=person, packageset=packageset,
110+ permission=ArchivePermissionType.UPLOAD)
111+ ap_set = getUtility(IArchivePermissionSet)
112+ self.assertEqual(
113+ 2,
114+ ap_set.uploadersForPackageset(archive_one, packageset_one).count())
115+ self.assertEqual(
116+ 1,
117+ ap_set.uploadersForPackageset(archive_two, packageset_one).count())
118+ self.assertEqual(
119+ 1,
120+ ap_set.uploadersForPackageset(archive_one, packageset_two).count())
121+ self.runDaily()
122+ self.assertEqual(
123+ 2,
124+ ap_set.uploadersForPackageset(archive_one, packageset_one).count())
125+ self.assertEqual(
126+ 1,
127+ ap_set.uploadersForPackageset(archive_two, packageset_one).count())
128+ self.assertEqual(
129+ 1,
130+ ap_set.uploadersForPackageset(archive_one, packageset_two).count())
131+
132+ def test_DuplicateArchivePermissionPruner_skips_non_packagesets(self):
133+ # DuplicateArchivePermissionPruner leaves non-packageset permissions
134+ # alone.
135+ switch_dbuser('testadmin')
136+ archive = self.factory.makeArchive()
137+ person = self.factory.makePerson()
138+ component = self.factory.makeComponent()
139+ for _ in range(3):
140+ ArchivePermission(
141+ archive=archive, person=person, component=component,
142+ permission=ArchivePermissionType.UPLOAD)
143+ ap_set = getUtility(IArchivePermissionSet)
144+ self.assertEqual(
145+ 3, ap_set.uploadersForComponent(archive, component).count())
146+ self.runDaily()
147+ self.assertEqual(
148+ 3, ap_set.uploadersForComponent(archive, component).count())
149+
150
151 class TestGarboTasks(TestCaseWithFactory):
152 layer = LaunchpadZopelessLayer