Merge lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/git-repository-delete-job
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/branch-delete-job
Diff against target: 752 lines (+317/-33)
10 files modified
database/schema/security.cfg (+7/-0)
lib/lp/code/configure.zcml (+9/-0)
lib/lp/code/enums.py (+12/-0)
lib/lp/code/interfaces/gitjob.py (+22/-1)
lib/lp/code/interfaces/gitrepository.py (+6/-0)
lib/lp/code/model/gitjob.py (+73/-0)
lib/lp/code/model/gitrepository.py (+18/-0)
lib/lp/code/model/tests/test_gitjob.py (+104/-0)
lib/lp/code/model/tests/test_gitrepository.py (+60/-32)
lib/lp/services/config/schema-lazr.conf (+6/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-repository-delete-job
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+364910@code.launchpad.net

Commit message

Add a Git repository deletion job.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This seems unnecessary since adding some more indexes has made deletion fast enough, so withdrawing this until we have a clear need.

Unmerged revisions

18913. By Colin Watson

Add a Git repository deletion job.

18912. By Colin Watson

Add a branch deletion job.

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 2019-03-21 16:22:19 +0000
3+++ database/schema/security.cfg 2019-03-21 16:22:19 +0000
4@@ -2621,6 +2621,13 @@
5 public.distributionsourcepackage = SELECT, DELETE
6 public.distroseries = SELECT
7 public.emailaddress = SELECT
8+public.gitactivity = SELECT, DELETE
9+public.gitjob = SELECT, INSERT, DELETE
10+public.gitref = SELECT, DELETE
11+public.gitrepository = SELECT, UPDATE, DELETE
12+public.gitrule = SELECT, DELETE
13+public.gitrulegrant = SELECT, DELETE
14+public.gitsubscription = SELECT, DELETE
15 public.job = SELECT, INSERT, UPDATE, DELETE
16 public.person = SELECT
17 public.previewdiff = SELECT, DELETE
18
19=== modified file 'lib/lp/code/configure.zcml'
20--- lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000
21+++ lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000
22@@ -1092,6 +1092,11 @@
23 provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">
24 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />
25 </securedutility>
26+ <securedutility
27+ component="lp.code.model.gitjob.GitRepositoryDeleteJob"
28+ provides="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource">
29+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource" />
30+ </securedutility>
31 <class class="lp.code.model.gitjob.GitRefScanJob">
32 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
33 <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />
34@@ -1104,6 +1109,10 @@
35 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
36 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />
37 </class>
38+ <class class="lp.code.model.gitjob.GitRepositoryDeleteJob">
39+ <allow interface="lp.code.interfaces.gitjob.IGitJob" />
40+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJob" />
41+ </class>
42
43 <lp:help-folder folder="help" name="+help-code" />
44
45
46=== modified file 'lib/lp/code/enums.py'
47--- lib/lp/code/enums.py 2019-03-21 16:22:19 +0000
48+++ lib/lp/code/enums.py 2019-03-21 16:22:19 +0000
49@@ -26,6 +26,7 @@
50 'GitGranteeType',
51 'GitObjectType',
52 'GitPermissionType',
53+ 'GitRepositoryDeletionStatus',
54 'GitRepositoryType',
55 'NON_CVS_RCS_TYPES',
56 'RevisionControlSystems',
57@@ -158,6 +159,17 @@
58 """)
59
60
61+class GitRepositoryDeletionStatus(DBEnumeratedType):
62+ """Git Repository Deletion Status
63+
64+ The deletion status of a repository is used to track asynchronous
65+ deletion.
66+ """
67+
68+ ACTIVE = DBItem(0, "Active")
69+ DELETING = DBItem(1, "Deleting")
70+
71+
72 class GitObjectType(DBEnumeratedType):
73 """Git Object Type
74
75
76=== modified file 'lib/lp/code/interfaces/gitjob.py'
77--- lib/lp/code/interfaces/gitjob.py 2015-09-01 17:10:46 +0000
78+++ lib/lp/code/interfaces/gitjob.py 2019-03-21 16:22:19 +0000
79@@ -9,6 +9,8 @@
80 'IGitJob',
81 'IGitRefScanJob',
82 'IGitRefScanJobSource',
83+ 'IGitRepositoryDeleteJob',
84+ 'IGitRepositoryDeleteJobSource',
85 'IGitRepositoryModifiedMailJob',
86 'IGitRepositoryModifiedMailJobSource',
87 'IReclaimGitRepositorySpaceJob',
88@@ -20,7 +22,10 @@
89 Attribute,
90 Interface,
91 )
92-from zope.schema import Text
93+from zope.schema import (
94+ Int,
95+ Text,
96+ )
97
98 from lp import _
99 from lp.code.interfaces.gitrepository import IGitRepository
100@@ -93,3 +98,19 @@
101 :param repository_delta: An `IGitRepositoryDelta` describing the
102 changes.
103 """
104+
105+
106+class IGitRepositoryDeleteJob(IRunnableJob):
107+ """A Job that deletes a repository from the database."""
108+
109+ repository_id = Int(title=_("The id of the repository to delete."))
110+
111+
112+class IGitRepositoryDeleteJobSource(IJobSource):
113+
114+ def create(repository, requester):
115+ """Delete a repository from the database.
116+
117+ :param repository: The `IGitRepository` to delete.
118+ :param requester: The `IPerson` who requested the deletion.
119+ """
120
121=== modified file 'lib/lp/code/interfaces/gitrepository.py'
122--- lib/lp/code/interfaces/gitrepository.py 2019-01-28 17:19:44 +0000
123+++ lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:22:19 +0000
124@@ -64,6 +64,7 @@
125 BranchSubscriptionDiffSize,
126 BranchSubscriptionNotificationLevel,
127 CodeReviewNotificationLevel,
128+ GitRepositoryDeletionStatus,
129 GitRepositoryType,
130 )
131 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
132@@ -139,6 +140,11 @@
133 "The way this repository is hosted: directly on Launchpad, or "
134 "imported from somewhere else.")))
135
136+ deletion_status = exported(Choice(
137+ title=_("Deletion status"), required=True, readonly=True,
138+ vocabulary=GitRepositoryDeletionStatus,
139+ description=_("The deletion status of this repository.")))
140+
141 registrant = exported(PublicPersonChoice(
142 title=_("Registrant"), required=True, readonly=True,
143 vocabulary="ValidPersonOrTeam",
144
145=== modified file 'lib/lp/code/model/gitjob.py'
146--- lib/lp/code/model/gitjob.py 2018-10-22 00:37:07 +0000
147+++ lib/lp/code/model/gitjob.py 2019-03-21 16:22:19 +0000
148@@ -8,6 +8,7 @@
149 'GitJob',
150 'GitJobType',
151 'GitRefScanJob',
152+ 'GitRepositoryDeleteJob',
153 'GitRepositoryModifiedMailJob',
154 'ReclaimGitRepositorySpaceJob',
155 ]
156@@ -25,6 +26,7 @@
157 SQL,
158 Store,
159 )
160+import transaction
161 from zope.component import getUtility
162 from zope.interface import (
163 implementer,
164@@ -36,17 +38,22 @@
165 from lp.code.enums import (
166 GitActivityType,
167 GitPermissionType,
168+ GitRepositoryDeletionStatus,
169 )
170+from lp.code.errors import CannotDeleteGitRepository
171 from lp.code.interfaces.githosting import IGitHostingClient
172 from lp.code.interfaces.gitjob import (
173 IGitJob,
174 IGitRefScanJob,
175 IGitRefScanJobSource,
176+ IGitRepositoryDeleteJob,
177+ IGitRepositoryDeleteJobSource,
178 IGitRepositoryModifiedMailJob,
179 IGitRepositoryModifiedMailJobSource,
180 IReclaimGitRepositorySpaceJob,
181 IReclaimGitRepositorySpaceJobSource,
182 )
183+from lp.code.interfaces.gitlookup import IGitLookup
184 from lp.code.interfaces.gitrule import describe_git_permissions
185 from lp.code.mail.branch import BranchMailer
186 from lp.registry.interfaces.person import IPersonSet
187@@ -99,6 +106,12 @@
188 modifications.
189 """)
190
191+ DELETE_REPOSITORY = DBItem(3, """
192+ Delete repository
193+
194+ This job deletes a repository from the database.
195+ """)
196+
197
198 @implementer(IGitJob)
199 class GitJob(StormBase):
200@@ -405,3 +418,63 @@
201 def run(self):
202 """See `IGitRepositoryModifiedMailJob`."""
203 self.getMailer().sendAll()
204+
205+
206+@implementer(IGitRepositoryDeleteJob)
207+@provider(IGitRepositoryDeleteJobSource)
208+class GitRepositoryDeleteJob(GitJobDerived):
209+ """A Job that deletes a repository from the database."""
210+
211+ class_job_type = GitJobType.DELETE_REPOSITORY
212+
213+ user_error_types = (CannotDeleteGitRepository,)
214+
215+ config = config.IGitRepositoryDeleteJobSource
216+
217+ def getOperationDescription(self):
218+ return "deleting a repository"
219+
220+ @classmethod
221+ def create(cls, repository, requester):
222+ """See `IGitRepositoryDeleteJobSource`."""
223+ metadata = {
224+ "repository_id": repository.id,
225+ "repository_name": repository.unique_name,
226+ }
227+ # The GitJob has a repository of None, because we don't want to
228+ # delete this job while trying to delete the repository.
229+ git_job = GitJob(
230+ None, cls.class_job_type, metadata, requester=requester)
231+ job = cls(git_job)
232+ job.celeryRunOnCommit()
233+ return job
234+
235+ @property
236+ def repository_id(self):
237+ return self.metadata["repository_id"]
238+
239+ def run(self):
240+ """See `IGitRepositoryDeleteJob`."""
241+ repository = getUtility(IGitLookup).get(self.repository_id)
242+ if repository is None:
243+ log.info(
244+ "Skipping repository %s because it has already been deleted." %
245+ self._cached_repository_name)
246+ elif (repository.deletion_status !=
247+ GitRepositoryDeletionStatus.DELETING):
248+ log.warning(
249+ "Skipping repository %s because its deletion status is not "
250+ "DELETING." % self._cached_repository_name)
251+ else:
252+ try:
253+ repository.destroySelf(break_references=True)
254+ except CannotDeleteGitRepository:
255+ # Set the deletion status back to ACTIVE so that it's
256+ # possible to try again. We don't attempt to undo the
257+ # renaming at the moment. Do this in its own transaction
258+ # since the job runner will abort the transaction.
259+ transaction.abort()
260+ removeSecurityProxy(repository).deletion_status = (
261+ GitRepositoryDeletionStatus.ACTIVE)
262+ transaction.commit()
263+ raise
264
265=== modified file 'lib/lp/code/model/gitrepository.py'
266--- lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000
267+++ lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000
268@@ -91,6 +91,7 @@
269 GitGranteeType,
270 GitObjectType,
271 GitPermissionType,
272+ GitRepositoryDeletionStatus,
273 GitRepositoryType,
274 )
275 from lp.code.errors import (
276@@ -282,6 +283,23 @@
277 repository_type = EnumCol(
278 dbName='repository_type', enum=GitRepositoryType, notNull=True)
279
280+ _deletion_status = EnumCol(
281+ dbName='deletion_status', enum=GitRepositoryDeletionStatus,
282+ default=GitRepositoryDeletionStatus.ACTIVE)
283+
284+ @property
285+ def deletion_status(self):
286+ # XXX cjwatson 2019-03-19: Remove once this column has been
287+ # backfilled.
288+ if self._deletion_status is None:
289+ return GitRepositoryDeletionStatus.ACTIVE
290+ else:
291+ return self._deletion_status
292+
293+ @deletion_status.setter
294+ def deletion_status(self, value):
295+ self._deletion_status = value
296+
297 registrant_id = Int(name='registrant', allow_none=False)
298 registrant = Reference(registrant_id, 'Person.id')
299
300
301=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
302--- lib/lp/code/model/tests/test_gitjob.py 2018-10-21 17:38:05 +0000
303+++ lib/lp/code/model/tests/test_gitjob.py 2019-03-21 16:22:19 +0000
304@@ -13,6 +13,10 @@
305 )
306 import hashlib
307
308+from fixtures import (
309+ FakeLogger,
310+ MockPatch,
311+ )
312 from lazr.lifecycle.snapshot import Snapshot
313 import pytz
314 from testtools.matchers import (
315@@ -23,6 +27,7 @@
316 MatchesStructure,
317 )
318 import transaction
319+from zope.component import getUtility
320 from zope.interface import providedBy
321 from zope.security.proxy import removeSecurityProxy
322
323@@ -30,6 +35,7 @@
324 from lp.code.enums import (
325 GitGranteeType,
326 GitObjectType,
327+ GitRepositoryDeletionStatus,
328 )
329 from lp.code.interfaces.branchmergeproposal import (
330 BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
331@@ -37,8 +43,11 @@
332 from lp.code.interfaces.gitjob import (
333 IGitJob,
334 IGitRefScanJob,
335+ IGitRepositoryDeleteJob,
336+ IGitRepositoryDeleteJobSource,
337 IReclaimGitRepositorySpaceJob,
338 )
339+from lp.code.interfaces.gitlookup import IGitLookup
340 from lp.code.model.gitjob import (
341 describe_repository_delta,
342 GitJob,
343@@ -52,6 +61,7 @@
344 from lp.services.database.constants import UTC_NOW
345 from lp.services.features.testing import FeatureFixture
346 from lp.services.job.runner import JobRunner
347+from lp.services.mail.sendmail import format_address_for_person
348 from lp.services.utils import seconds_since_epoch
349 from lp.services.webapp import canonical_url
350 from lp.services.webapp.snapshot import notify_modified
351@@ -65,6 +75,7 @@
352 DatabaseFunctionalLayer,
353 ZopelessDatabaseLayer,
354 )
355+from lp.testing.mail_helpers import pop_notifications
356
357
358 class TestGitJob(TestCaseWithFactory):
359@@ -473,5 +484,98 @@
360 snapshot, repository)
361
362
363+class TestGitRepositoryDeleteJob(TestCaseWithFactory):
364+
365+ layer = ZopelessDatabaseLayer
366+
367+ def test_providesInterface(self):
368+ repository = self.factory.makeGitRepository()
369+ requester = repository.registrant
370+ job = getUtility(IGitRepositoryDeleteJobSource).create(
371+ repository, requester)
372+ self.assertProvides(job, IGitRepositoryDeleteJob)
373+
374+ def test_run(self):
375+ repository = self.factory.makeGitRepository()
376+ repository_id = repository.id
377+ requester = repository.registrant
378+ job = getUtility(IGitRepositoryDeleteJobSource).create(
379+ repository, requester)
380+ removeSecurityProxy(repository).deletion_status = (
381+ GitRepositoryDeletionStatus.DELETING)
382+ logger = self.useFixture(FakeLogger())
383+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
384+ job.run()
385+ self.assertEqual('', logger.output)
386+ self.assertIsNone(getUtility(IGitLookup).get(repository_id))
387+
388+ def test_already_deleted(self):
389+ repository = self.factory.makeGitRepository()
390+ repository_name = repository.unique_name
391+ requester = repository.registrant
392+ job = getUtility(IGitRepositoryDeleteJobSource).create(
393+ repository, requester)
394+ repository.destroySelf()
395+ logger = self.useFixture(FakeLogger())
396+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
397+ job.run()
398+ self.assertEqual(
399+ "Skipping repository %s because it has already been "
400+ "deleted.\n" % repository_name,
401+ logger.output)
402+
403+ def test_not_deleting(self):
404+ # The job skips repositories that aren't DELETING. This shouldn't
405+ # be possible in practice, but is a guard against accidents.
406+ repository = self.factory.makeGitRepository()
407+ repository_id = repository.id
408+ repository_name = repository.unique_name
409+ self.assertNotEqual(
410+ GitRepositoryDeletionStatus.DELETING, repository.deletion_status)
411+ requester = repository.registrant
412+ job = getUtility(IGitRepositoryDeleteJobSource).create(
413+ repository, requester)
414+ logger = self.useFixture(FakeLogger())
415+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
416+ job.run()
417+ self.assertEqual(
418+ "Skipping repository %s because its deletion status is not "
419+ "DELETING.\n" % repository_name,
420+ logger.output)
421+ self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
422+
423+ def test_error(self):
424+ # If deleting the repository fails, an error message is sent to the
425+ # requester and the deletion status is set back to ACTIVE. This
426+ # can't normally happen because the job always breaks references to
427+ # the repository, so we patch in a failure to allow testing the
428+ # error path.
429+ repository = self.factory.makeGitRepository()
430+ repository_id = repository.id
431+ requester = repository.registrant
432+ job = getUtility(IGitRepositoryDeleteJobSource).create(
433+ repository, requester)
434+ removeSecurityProxy(repository).deletion_status = (
435+ GitRepositoryDeletionStatus.DELETING)
436+ logger = self.useFixture(FakeLogger())
437+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
438+ with MockPatch(
439+ "lp.code.model.gitrepository.GitRepository.canBeDeleted",
440+ return_value=False):
441+ JobRunner([job]).runJobHandleError(job)
442+ self.assertIn(
443+ "failed with user error CannotDeleteGitRepository", logger.output)
444+ self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
445+ self.assertEqual(
446+ GitRepositoryDeletionStatus.ACTIVE, repository.deletion_status)
447+ self.assertEqual([], self.oopses)
448+ [mail] = pop_notifications()
449+ self.assertEqual(format_address_for_person(requester), mail["to"])
450+ self.assertEqual(
451+ "Launchpad error while deleting a repository", mail["subject"])
452+ self.assertIn(
453+ "Cannot delete Git repository", mail.get_payload(decode=True))
454+
455+
456 # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,
457 # but that isn't feasible until we have a proper turnip fixture.
458
459=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
460--- lib/lp/code/model/tests/test_gitrepository.py 2019-02-11 12:31:06 +0000
461+++ lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:22:19 +0000
462@@ -638,7 +638,8 @@
463 self.repository.canBeDeleted(),
464 "A newly created repository should be able to be deleted.")
465 repository_id = self.repository.id
466- self.repository.destroySelf()
467+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
468+ self.repository.destroySelf()
469 self.assertIsNone(
470 getUtility(IGitLookup).get(repository_id),
471 "The repository has not been deleted.")
472@@ -650,7 +651,8 @@
473 CodeReviewNotificationLevel.NOEMAIL, self.user)
474 self.assertTrue(self.repository.canBeDeleted())
475 repository_id = self.repository.id
476- self.repository.destroySelf()
477+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
478+ self.repository.destroySelf()
479 self.assertIsNone(
480 getUtility(IGitLookup).get(repository_id),
481 "The repository has not been deleted.")
482@@ -665,7 +667,8 @@
483 CodeReviewNotificationLevel.NOEMAIL, self.user)
484 self.assertTrue(self.repository.canBeDeleted())
485 repository_id = self.repository.id
486- self.repository.destroySelf()
487+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
488+ self.repository.destroySelf()
489 self.assertIsNone(
490 getUtility(IGitLookup).get(repository_id),
491 "The repository has not been deleted.")
492@@ -687,8 +690,9 @@
493 self.assertFalse(
494 self.repository.canBeDeleted(),
495 "A repository with a landing target is not deletable.")
496- self.assertRaises(
497- CannotDeleteGitRepository, self.repository.destroySelf)
498+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
499+ self.assertRaises(
500+ CannotDeleteGitRepository, self.repository.destroySelf)
501
502 def test_landing_candidate_disables_deletion(self):
503 # A repository with a landing candidate cannot be deleted.
504@@ -698,8 +702,9 @@
505 self.assertFalse(
506 self.repository.canBeDeleted(),
507 "A repository with a landing candidate is not deletable.")
508- self.assertRaises(
509- CannotDeleteGitRepository, self.repository.destroySelf)
510+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
511+ self.assertRaises(
512+ CannotDeleteGitRepository, self.repository.destroySelf)
513
514 def test_prerequisite_repository_disables_deletion(self):
515 # A repository that is a prerequisite repository cannot be deleted.
516@@ -711,14 +716,16 @@
517 self.assertFalse(
518 self.repository.canBeDeleted(),
519 "A repository with a prerequisite target is not deletable.")
520- self.assertRaises(
521- CannotDeleteGitRepository, self.repository.destroySelf)
522+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
523+ self.assertRaises(
524+ CannotDeleteGitRepository, self.repository.destroySelf)
525
526 def test_related_GitJobs_deleted(self):
527 # A repository with an associated job will delete those jobs.
528 GitAPI(None, None).notify(self.repository.getInternalPath())
529 store = Store.of(self.repository)
530- self.repository.destroySelf()
531+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
532+ self.repository.destroySelf()
533 # Need to commit the transaction to fire off the constraint checks.
534 transaction.commit()
535 jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN)
536@@ -729,7 +736,8 @@
537 # to remove the repository from disk as well.
538 repository_path = self.repository.getInternalPath()
539 store = Store.of(self.repository)
540- self.repository.destroySelf()
541+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
542+ self.repository.destroySelf()
543 jobs = store.find(
544 GitJob,
545 GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE)
546@@ -742,14 +750,16 @@
547 # If repository is a base_git_repository in a recipe, it is deleted.
548 recipe = self.factory.makeSourcePackageRecipe(
549 branches=self.factory.makeGitRefs(owner=self.user))
550- recipe.base_git_repository.destroySelf(break_references=True)
551+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
552+ recipe.base_git_repository.destroySelf(break_references=True)
553
554 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
555 # If repository is referred to by a recipe, it is deleted.
556 [ref1] = self.factory.makeGitRefs(owner=self.user)
557 [ref2] = self.factory.makeGitRefs(owner=self.user)
558 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])
559- ref2.repository.destroySelf(break_references=True)
560+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
561+ ref2.repository.destroySelf(break_references=True)
562
563 def test_destroySelf_with_inline_comments_draft(self):
564 # Draft inline comments related to a deleted repository (source or
565@@ -763,7 +773,8 @@
566 previewdiff_id=preview_diff.id,
567 person=self.user,
568 comments={"1": "Should vanish."})
569- self.repository.destroySelf(break_references=True)
570+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
571+ self.repository.destroySelf(break_references=True)
572
573 def test_destroySelf_with_inline_comments_published(self):
574 # Published inline comments related to a deleted repository (source
575@@ -779,12 +790,14 @@
576 previewdiff_id=preview_diff.id,
577 inline_comments={"1": "Must disappear."},
578 )
579- self.repository.destroySelf(break_references=True)
580+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
581+ self.repository.destroySelf(break_references=True)
582
583 def test_related_webhooks_deleted(self):
584 webhook = self.factory.makeWebhook(target=self.repository)
585 webhook.ping()
586- self.repository.destroySelf()
587+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
588+ self.repository.destroySelf()
589 transaction.commit()
590 self.assertRaises(LostObjectError, getattr, webhook, 'target')
591
592@@ -796,7 +809,8 @@
593 activities = store.find(
594 GitActivity, GitActivity.repository_id == repository_id)
595 self.assertNotEqual([], list(activities))
596- self.repository.destroySelf()
597+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
598+ self.repository.destroySelf()
599 transaction.commit()
600 self.assertRaises(LostObjectError, getattr, grant, 'rule')
601 self.assertRaises(LostObjectError, getattr, rule, 'repository')
602@@ -895,7 +909,8 @@
603 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
604 merge_proposal1_id = merge_proposal1.id
605 BranchMergeProposal.get(merge_proposal1_id)
606- self.repository.destroySelf(break_references=True)
607+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
608+ self.repository.destroySelf(break_references=True)
609 self.assertRaises(
610 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal1_id)
611
612@@ -905,8 +920,9 @@
613 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
614 merge_proposal1_id = merge_proposal1.id
615 BranchMergeProposal.get(merge_proposal1_id)
616- merge_proposal1.target_git_repository.destroySelf(
617- break_references=True)
618+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
619+ merge_proposal1.target_git_repository.destroySelf(
620+ break_references=True)
621 self.assertRaises(SQLObjectNotFound,
622 BranchMergeProposal.get, merge_proposal1_id)
623
624@@ -914,8 +930,9 @@
625 # Merge proposal prerequisite repositories can be deleted with
626 # break_references.
627 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
628- merge_proposal1.prerequisite_git_repository.destroySelf(
629- break_references=True)
630+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
631+ merge_proposal1.prerequisite_git_repository.destroySelf(
632+ break_references=True)
633 self.assertIsNone(merge_proposal1.prerequisite_git_repository)
634
635 def test_delete_source_CodeReviewComment(self):
636@@ -923,7 +940,8 @@
637 comment = self.factory.makeCodeReviewComment(git=True)
638 comment_id = comment.id
639 repository = comment.branch_merge_proposal.source_git_repository
640- repository.destroySelf(break_references=True)
641+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
642+ repository.destroySelf(break_references=True)
643 self.assertRaises(
644 SQLObjectNotFound, CodeReviewComment.get, comment_id)
645
646@@ -932,7 +950,8 @@
647 comment = self.factory.makeCodeReviewComment(git=True)
648 comment_id = comment.id
649 repository = comment.branch_merge_proposal.target_git_repository
650- repository.destroySelf(break_references=True)
651+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
652+ repository.destroySelf(break_references=True)
653 self.assertRaises(
654 SQLObjectNotFound, CodeReviewComment.get, comment_id)
655
656@@ -941,14 +960,18 @@
657 merge_proposal = self.factory.makeBranchMergeProposalForGit()
658 merge_proposal.nominateReviewer(
659 self.factory.makePerson(), self.factory.makePerson())
660- merge_proposal.source_git_repository.destroySelf(break_references=True)
661+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
662+ merge_proposal.source_git_repository.destroySelf(
663+ break_references=True)
664
665 def test_targetBranchWithCodeReviewVoteReference(self):
666 # break_references handles CodeReviewVoteReference target repository.
667 merge_proposal = self.factory.makeBranchMergeProposalForGit()
668 merge_proposal.nominateReviewer(
669 self.factory.makePerson(), self.factory.makePerson())
670- merge_proposal.target_git_repository.destroySelf(break_references=True)
671+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
672+ merge_proposal.target_git_repository.destroySelf(
673+ break_references=True)
674
675 def test_code_import_requirements(self):
676 # Code imports are not included explicitly in deletion requirements.
677@@ -967,7 +990,8 @@
678 code_import = self.factory.makeCodeImport(
679 target_rcs_type=TargetRevisionControlSystems.GIT)
680 code_import_id = code_import.id
681- code_import.git_repository.destroySelf(break_references=True)
682+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
683+ code_import.git_repository.destroySelf(break_references=True)
684 self.assertRaises(
685 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
686
687@@ -988,7 +1012,8 @@
688 repository=repository, paths=["refs/heads/1", "refs/heads/2"])
689 snap1 = self.factory.makeSnap(git_ref=ref1)
690 snap2 = self.factory.makeSnap(git_ref=ref2)
691- repository.destroySelf(break_references=True)
692+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
693+ repository.destroySelf(break_references=True)
694 transaction.commit()
695 self.assertIsNone(snap1.git_repository)
696 self.assertIsNone(snap1.git_path)
697@@ -1001,7 +1026,8 @@
698 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
699 with person_logged_in(
700 merge_proposal.prerequisite_git_repository.owner):
701- ClearPrerequisiteRepository(merge_proposal)()
702+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
703+ ClearPrerequisiteRepository(merge_proposal)()
704 self.assertIsNone(merge_proposal.prerequisite_git_repository)
705
706 def test_DeletionOperation(self):
707@@ -1012,8 +1038,9 @@
708 # DeletionCallable must invoke the callable.
709 merge_proposal = self.factory.makeBranchMergeProposalForGit()
710 merge_proposal_id = merge_proposal.id
711- DeletionCallable(
712- merge_proposal, "blah", merge_proposal.deleteProposal)()
713+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
714+ DeletionCallable(
715+ merge_proposal, "blah", merge_proposal.deleteProposal)()
716 self.assertRaises(
717 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal_id)
718
719@@ -1023,7 +1050,8 @@
720 code_import = self.factory.makeCodeImport(
721 target_rcs_type=TargetRevisionControlSystems.GIT)
722 code_import_id = code_import.id
723- DeleteCodeImport(code_import)()
724+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
725+ DeleteCodeImport(code_import)()
726 self.assertRaises(
727 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
728
729
730=== modified file 'lib/lp/services/config/schema-lazr.conf'
731--- lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000
732+++ lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000
733@@ -1871,6 +1871,7 @@
734 IBranchModifiedMailJobSource,
735 ICommercialExpiredJobSource,
736 IExpiringMembershipNotificationJobSource,
737+ IGitRepositoryDeleteJobSource,
738 IGitRepositoryModifiedMailJobSource,
739 IMembershipNotificationJobSource,
740 IPackageUploadNotificationJobSource,
741@@ -1931,6 +1932,11 @@
742 module: lp.code.interfaces.gitjob
743 dbuser: branchscanner
744
745+[IGitRepositoryDeleteJobSource]
746+module: lp.code.interfaces.gitjob
747+dbuser: branch-delete-job
748+crontab_group: MAIN
749+
750 [IGitRepositoryModifiedMailJobSource]
751 module: lp.code.interfaces.gitjob
752 dbuser: send-branch-mail