Merge lp:~wgrant/launchpad/archive-defer-deletion into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 16499
Proposed branch: lp:~wgrant/launchpad/archive-defer-deletion
Merge into: lp:launchpad
Diff against target: 324 lines (+110/-52)
7 files modified
lib/lp/archivepublisher/publishing.py (+29/-0)
lib/lp/archivepublisher/tests/test_publisher.py (+32/-0)
lib/lp/soyuz/doc/archive-deletion.txt (+9/-31)
lib/lp/soyuz/interfaces/publishing.py (+5/-3)
lib/lp/soyuz/model/archive.py (+13/-10)
lib/lp/soyuz/model/publishing.py (+16/-8)
lib/lp/soyuz/tests/test_publishing.py (+6/-0)
To merge this branch: bzr merge lp:~wgrant/launchpad/archive-defer-deletion
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+148988@code.launchpad.net

Commit message

Defer PPA publication deletion to the publisher, remove NBS binaries, and allow files to expire.

Description of the change

This branch fixes three archive deletion bugs.

Firstly, it was marking all publications deleted in the webapp request, which is obviously inappropriate as it's an unbounded number of writes. It's also racy, as the archive is disabled in the same transaction so packages may end up successfully uploading slightly after the other publications are deleted. I moved the publication deletion phase to the publisher. Since we don't have the deletion requester at that point, they're now attributed to the janitor.

Secondly, PPA deletion left BPPHs around if there was no corresponding active SPPH (usually an NBS case). I fixed this by also deleting any remaining binaries.

Thirdly, while the files were removed from disk, dateremoved wasn't being set. This prevents any package published in a deleted archive from ever being expired. I adjusted deletion to also set dateremoved in this case, since we don't need to wait for p-d-r to remove the files.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/publishing.py'
2--- lib/lp/archivepublisher/publishing.py 2012-09-07 05:13:57 +0000
3+++ lib/lp/archivepublisher/publishing.py 2013-02-20 02:37:30 +0000
4@@ -20,7 +20,9 @@
5 _multivalued,
6 Release,
7 )
8+from zope.component import getUtility
9
10+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
11 from lp.archivepublisher import HARDCODED_COMPONENT_ORDER
12 from lp.archivepublisher.config import getPubConfig
13 from lp.archivepublisher.diskpool import DiskPool
14@@ -40,6 +42,7 @@
15 )
16 from lp.registry.interfaces.pocket import PackagePublishingPocket
17 from lp.registry.interfaces.series import SeriesStatus
18+from lp.services.database.constants import UTC_NOW
19 from lp.services.database.sqlbase import sqlvalues
20 from lp.services.librarian.client import LibrarianClient
21 from lp.services.utils import file_exists
22@@ -49,6 +52,10 @@
23 BinaryPackageFormat,
24 PackagePublishingStatus,
25 )
26+from lp.soyuz.interfaces.publishing import (
27+ active_publishing_status,
28+ IPublishingSet,
29+ )
30 from lp.soyuz.model.publishing import (
31 BinaryPackagePublishingHistory,
32 SourcePackagePublishingHistory,
33@@ -747,6 +754,28 @@
34 "Attempting to delete archive '%s/%s' at '%s'." % (
35 self.archive.owner.name, self.archive.name, root_dir))
36
37+ # Set all the publications to DELETED.
38+ sources = self.archive.getPublishedSources(
39+ status=active_publishing_status)
40+ getUtility(IPublishingSet).requestDeletion(
41+ sources, removed_by=getUtility(ILaunchpadCelebrities).janitor,
42+ removal_comment="Removed when deleting archive")
43+
44+ # Deleting the sources will have killed the corresponding
45+ # binaries too, but there may be orphaned leftovers (eg. NBS).
46+ binaries = self.archive.getAllPublishedBinaries(
47+ status=active_publishing_status)
48+ getUtility(IPublishingSet).requestDeletion(
49+ binaries, removed_by=getUtility(ILaunchpadCelebrities).janitor,
50+ removal_comment="Removed when deleting archive")
51+
52+ # Now set dateremoved on any publication that doesn't already
53+ # have it set, so things can expire from the librarian.
54+ for pub in self.archive.getPublishedSources(include_removed=False):
55+ pub.dateremoved = UTC_NOW
56+ for pub in self.archive.getAllPublishedBinaries(include_removed=False):
57+ pub.dateremoved = UTC_NOW
58+
59 for directory in (root_dir, self._config.metaroot):
60 if not os.path.exists(directory):
61 continue
62
63=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
64--- lib/lp/archivepublisher/tests/test_publisher.py 2012-08-17 11:12:37 +0000
65+++ lib/lp/archivepublisher/tests/test_publisher.py 2013-02-20 02:37:30 +0000
66@@ -148,6 +148,24 @@
67 test_archive = getUtility(IArchiveSet).new(
68 distribution=self.ubuntutest, owner=ubuntu_team,
69 purpose=ArchivePurpose.PPA)
70+
71+ # Create some source and binary publications, including an
72+ # orphaned NBS binary.
73+ spph = self.factory.makeSourcePackagePublishingHistory(
74+ archive=test_archive)
75+ bpph = self.factory.makeBinaryPackagePublishingHistory(
76+ archive=test_archive)
77+ orphaned_bpph = self.factory.makeBinaryPackagePublishingHistory(
78+ archive=test_archive)
79+ bpb = orphaned_bpph.binarypackagerelease.build
80+ bpb.current_source_publication.supersede()
81+ dead_spph = self.factory.makeSourcePackagePublishingHistory(
82+ archive=test_archive)
83+ dead_spph.supersede()
84+ dead_bpph = self.factory.makeBinaryPackagePublishingHistory(
85+ archive=test_archive)
86+ dead_bpph.supersede()
87+
88 publisher = getPublisher(test_archive, None, self.logger)
89
90 self.assertTrue(os.path.exists(publisher._config.archiveroot))
91@@ -168,6 +186,20 @@
92 self.assertEqual(ArchiveStatus.DELETED, test_archive.status)
93 self.assertEqual(False, test_archive.publish)
94
95+ # All of the archive's active publications have been marked
96+ # DELETED, and dateremoved has been set early because they've
97+ # already been removed from disk.
98+ for pub in (spph, bpph, orphaned_bpph):
99+ self.assertEqual(PackagePublishingStatus.DELETED, pub.status)
100+ self.assertEqual(u'janitor', pub.removed_by.name)
101+ self.assertIsNot(None, pub.dateremoved)
102+
103+ # The SUPERSEDED publications now have dateremoved set, even
104+ # though p-d-r hasn't run over them.
105+ for pub in (dead_spph, dead_bpph):
106+ self.assertIs(None, pub.scheduleddeletiondate)
107+ self.assertIsNot(None, pub.dateremoved)
108+
109 # Trying to delete it again won't fail, in the corner case where
110 # some admin manually deleted the repo.
111 publisher.deleteArchive()
112
113=== modified file 'lib/lp/soyuz/doc/archive-deletion.txt'
114--- lib/lp/soyuz/doc/archive-deletion.txt 2012-04-10 14:01:17 +0000
115+++ lib/lp/soyuz/doc/archive-deletion.txt 2013-02-20 02:37:30 +0000
116@@ -1,17 +1,13 @@
117 = Deleting an archive =
118
119 When deleting an archive, the user calls IArchive.delete(), passing in
120-the IPerson who is requesting the deletion.
121-
122-All of the publishing records will be marked as DELETED, the archive is
123-disabled and the status is set to DELETING.
124-
125-This status tells the publisher to then delete the repository area. Once
126-it completes that task it will set the status to DELETED.
127-
128- >>> from lp.soyuz.enums import ArchiveStatus
129- >>> from lp.soyuz.interfaces.archive import (
130- ... IArchiveSet, IArchive)
131+the IPerson who is requesting the deletion. The archive is disabled and
132+the status set to DELETING.
133+
134+This status tells the publisher to then set the publications to DELETED
135+and delete the repository area. Once it completes that task the status
136+of the archive itself is set to DELETED.
137+
138 >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
139 >>> login("admin@canonical.com")
140 >>> stp = SoyuzTestPublisher()
141@@ -55,26 +51,8 @@
142 >>> ignored = login_person(archive.owner)
143 >>> archive.delete(archive.owner)
144
145-The deletion code uses a store.execute() command to speed up the operation
146-where many records need to be updated. Therefore we need to invalidate
147-the cache to make Storm re-read the database objects.
148-
149- >>> Store.of(archive).invalidate()
150-
151-Now, all the publications are DELETED, the archive is disabled and the
152-status is DELETING to tell the publisher to remove the repository:
153-
154- >>> publications = list(archive.getPublishedSources())
155- >>> publications.extend(list(archive.getAllPublishedBinaries()))
156- >>> for pub in publications:
157- ... print "%s, %s by %s" % (
158- ... pub.displayname, pub.status.name, pub.removed_by.name)
159- foo 666 in breezy-autotest, DELETED by archive-owner
160- foo 666 in breezy-autotest, DELETED by archive-owner
161- foo-bin1 666 in breezy-autotest i386, DELETED by archive-owner
162- foo-bin1 666 in breezy-autotest hppa, DELETED by archive-owner
163- foo-bin2 666 in breezy-autotest i386, DELETED by archive-owner
164- foo-bin2 666 in breezy-autotest hppa, DELETED by archive-owner
165+Now the archive is disabled and the status is DELETING to tell the
166+publisher to remove the publications and the repository:
167
168 >>> print archive.enabled
169 False
170
171=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
172--- lib/lp/soyuz/interfaces/publishing.py 2013-01-31 02:02:37 +0000
173+++ lib/lp/soyuz/interfaces/publishing.py 2013-02-20 02:37:30 +0000
174@@ -1268,13 +1268,15 @@
175 This is a supporting operation for a deletion request.
176 """
177
178- def requestDeletion(sources, removed_by, removal_comment=None):
179+ def requestDeletion(pub, removed_by, removal_comment=None):
180 """Delete the source and binary publications specified.
181
182 This method deletes the source publications passed via the first
183- parameter as well as their associated binary publications.
184+ parameter as well as their associated binary publications, and any
185+ binary publications passed in.
186
187- :param sources: list of `SourcePackagePublishingHistory` objects.
188+ :param pubs: list of `SourcePackagePublishingHistory` and
189+ `BinaryPackagePublishingHistory` objects.
190 :param removed_by: `IPerson` responsible for the removal.
191 :param removal_comment: optional text describing the removal reason.
192
193
194=== modified file 'lib/lp/soyuz/model/archive.py'
195--- lib/lp/soyuz/model/archive.py 2013-02-06 10:40:16 +0000
196+++ lib/lp/soyuz/model/archive.py 2013-02-20 02:37:30 +0000
197@@ -493,7 +493,8 @@
198 def getPublishedSources(self, name=None, version=None, status=None,
199 distroseries=None, pocket=None,
200 exact_match=False, created_since_date=None,
201- eager_load=False, component_name=None):
202+ eager_load=False, component_name=None,
203+ include_removed=True):
204 """See `IArchive`."""
205 # clauses contains literal sql expressions for things that don't work
206 # easily in storm : this method was migrated from sqlobject but some
207@@ -560,6 +561,9 @@
208 SourcePackagePublishingHistory.datecreated >=
209 created_since_date)
210
211+ if not include_removed:
212+ clauses.append(SourcePackagePublishingHistory.dateremoved == None)
213+
214 store = Store.of(self)
215 resultset = store.find(
216 SourcePackagePublishingHistory, *clauses).order_by(*orderBy)
217@@ -703,7 +707,7 @@
218 def _getBinaryPublishingBaseClauses(
219 self, name=None, version=None, status=None, distroarchseries=None,
220 pocket=None, exact_match=False, created_since_date=None,
221- ordered=True):
222+ ordered=True, include_removed=True):
223 """Base clauses and clauseTables for binary publishing queries.
224
225 Returns a list of 'clauses' (to be joined in the callsite) and
226@@ -781,17 +785,22 @@
227 "BinaryPackagePublishingHistory.datecreated >= %s"
228 % sqlvalues(created_since_date))
229
230+ if not include_removed:
231+ clauses.append(
232+ "BinaryPackagePublishingHistory.dateremoved IS NULL")
233+
234 return clauses, clauseTables, orderBy
235
236 def getAllPublishedBinaries(self, name=None, version=None, status=None,
237 distroarchseries=None, pocket=None,
238 exact_match=False, created_since_date=None,
239- ordered=True):
240+ ordered=True, include_removed=True):
241 """See `IArchive`."""
242 clauses, clauseTables, orderBy = self._getBinaryPublishingBaseClauses(
243 name=name, version=version, status=status, pocket=pocket,
244 distroarchseries=distroarchseries, exact_match=exact_match,
245- created_since_date=created_since_date, ordered=ordered)
246+ created_since_date=created_since_date, ordered=ordered,
247+ include_removed=include_removed)
248
249 all_binaries = BinaryPackagePublishingHistory.select(
250 ' AND '.join(clauses), clauseTables=clauseTables,
251@@ -1971,12 +1980,6 @@
252 ArchiveStatus.DELETING, ArchiveStatus.DELETED,
253 "This archive is already deleted.")
254
255- # Set all the publications to DELETED.
256- sources = self.getPublishedSources(status=active_publishing_status)
257- getUtility(IPublishingSet).requestDeletion(
258- sources, removed_by=deleted_by,
259- removal_comment="Removed when deleting archive")
260-
261 # Mark the archive's status as DELETING so the repository can be
262 # removed by the publisher.
263 self.status = ArchiveStatus.DELETING
264
265=== modified file 'lib/lp/soyuz/model/publishing.py'
266--- lib/lp/soyuz/model/publishing.py 2013-02-06 07:25:12 +0000
267+++ lib/lp/soyuz/model/publishing.py 2013-02-20 02:37:30 +0000
268@@ -1992,11 +1992,18 @@
269 removed_byID=removed_by_id,
270 removal_comment=removal_comment)
271
272- def requestDeletion(self, sources, removed_by, removal_comment=None):
273+ def requestDeletion(self, pubs, removed_by, removal_comment=None):
274 """See `IPublishingSet`."""
275- sources = list(sources)
276- if len(sources) == 0:
277+ pubs = list(pubs)
278+ sources = [
279+ pub for pub in pubs
280+ if ISourcePackagePublishingHistory.providedBy(pub)]
281+ binaries = [
282+ pub for pub in pubs
283+ if IBinaryPackagePublishingHistory.providedBy(pub)]
284+ if not sources and not binaries:
285 return
286+ assert len(sources) + len(binaries) == len(pubs)
287
288 spph_ids = [spph.id for spph in sources]
289 self.setMultipleDeleted(
290@@ -2005,11 +2012,12 @@
291
292 getUtility(IDistroSeriesDifferenceJobSource).createForSPPHs(sources)
293
294- # Mark binary publications deleted.
295- bpph_ids = [
296- bpph.id
297- for source, bpph, bin, bin_name, arch
298- in self.getBinaryPublicationsForSources(sources)]
299+ # Append the sources' related binaries to our condemned list,
300+ # and mark them all deleted.
301+ bpph_ids = [bpph.id for bpph in binaries]
302+ bpph_ids.extend(
303+ bpph.id for source, bpph, bin, bin_name, arch
304+ in self.getBinaryPublicationsForSources(sources))
305 if len(bpph_ids) > 0:
306 self.setMultipleDeleted(
307 BinaryPackagePublishingHistory, bpph_ids, removed_by,
308
309=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
310--- lib/lp/soyuz/tests/test_publishing.py 2013-02-14 01:10:48 +0000
311+++ lib/lp/soyuz/tests/test_publishing.py 2013-02-20 02:37:30 +0000
312@@ -1184,6 +1184,12 @@
313 [other_spph], self.factory.makePerson())
314 self.assertEqual(PackagePublishingStatus.PENDING, spph.status)
315
316+ def test_requestDeletion_marks_BPPHs_deleted(self):
317+ bpph = self.factory.makeBinaryPackagePublishingHistory()
318+ getUtility(IPublishingSet).requestDeletion(
319+ [bpph], self.factory.makePerson())
320+ self.assertEqual(PackagePublishingStatus.DELETED, bpph.status)
321+
322 def test_requestDeletion_marks_attached_BPPHs_deleted(self):
323 bpph = self.factory.makeBinaryPackagePublishingHistory()
324 spph = self.factory.makeSPPHForBPPH(bpph)