Merge ~cjwatson/launchpad:charm-recipe-delete-builds into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 1138da210f8031e3e23c46cada1dc28574ebe8e6
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:charm-recipe-delete-builds
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:charm-recipe-listing-views
Diff against target: 291 lines (+154/-10)
3 files modified
lib/lp/charms/interfaces/charmrecipe.py (+3/-0)
lib/lp/charms/model/charmrecipe.py (+49/-4)
lib/lp/charms/tests/test_charmrecipe.py (+102/-6)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Review via email: mp+403959@code.launchpad.net

Commit message

Delete charm recipe builds and jobs when deleting recipes

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py
2index c972097..9c76cf4 100644
3--- a/lib/lp/charms/interfaces/charmrecipe.py
4+++ b/lib/lp/charms/interfaces/charmrecipe.py
5@@ -513,6 +513,9 @@ class ICharmRecipeSet(Interface):
6 def getByName(owner, project, name):
7 """Returns the appropriate `ICharmRecipe` for the given objects."""
8
9+ def exists(owner, project, name):
10+ """Check to see if a matching charm recipe exists."""
11+
12 def findByPerson(person, visible_by_user=None):
13 """Return all charm recipes relevant to `person`.
14
15diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
16index d9c9b5c..f2066d4 100644
17--- a/lib/lp/charms/model/charmrecipe.py
18+++ b/lib/lp/charms/model/charmrecipe.py
19@@ -20,6 +20,7 @@ from lazr.lifecycle.event import ObjectCreatedEvent
20 import pytz
21 from storm.databases.postgres import JSON
22 from storm.locals import (
23+ And,
24 Bool,
25 DateTime,
26 Desc,
27@@ -28,6 +29,7 @@ from storm.locals import (
28 Not,
29 Or,
30 Reference,
31+ Select,
32 Store,
33 Unicode,
34 )
35@@ -46,6 +48,8 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities
36 from lp.buildmaster.enums import BuildStatus
37 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
38 from lp.buildmaster.model.builder import Builder
39+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
40+from lp.buildmaster.model.buildqueue import BuildQueue
41 from lp.charms.adapters.buildarch import determine_instances_to_build
42 from lp.charms.interfaces.charmrecipe import (
43 BadCharmRecipeSearchContext,
44@@ -67,12 +71,14 @@ from lp.charms.interfaces.charmrecipe import (
45 ICharmRecipeSet,
46 MissingCharmcraftYaml,
47 NoSourceForCharmRecipe,
48+ NoSuchCharmRecipe,
49 )
50 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
51 from lp.charms.interfaces.charmrecipejob import (
52 ICharmRecipeRequestBuildsJobSource,
53 )
54 from lp.charms.model.charmrecipebuild import CharmRecipeBuild
55+from lp.charms.model.charmrecipejob import CharmRecipeJob
56 from lp.code.errors import (
57 GitRepositoryBlobNotFound,
58 GitRepositoryScanFault,
59@@ -116,6 +122,7 @@ from lp.services.database.stormexpr import (
60 )
61 from lp.services.features import getFeatureFlag
62 from lp.services.job.interfaces.job import JobStatus
63+from lp.services.job.model.job import Job
64 from lp.services.librarian.model import LibraryFileAlias
65 from lp.services.propertycache import (
66 cachedproperty,
67@@ -639,7 +646,35 @@ class CharmRecipe(StormBase):
68
69 def destroySelf(self):
70 """See `ICharmRecipe`."""
71- IStore(CharmRecipe).remove(self)
72+ store = IStore(self)
73+ # Remove build jobs. There won't be many queued builds, so we can
74+ # afford to do this the safe but slow way via BuildQueue.destroySelf
75+ # rather than in bulk.
76+ buildqueue_records = store.find(
77+ BuildQueue,
78+ BuildQueue._build_farm_job_id ==
79+ CharmRecipeBuild.build_farm_job_id,
80+ CharmRecipeBuild.recipe == self)
81+ for buildqueue_record in buildqueue_records:
82+ buildqueue_record.destroySelf()
83+ build_farm_job_ids = list(store.find(
84+ CharmRecipeBuild.build_farm_job_id,
85+ CharmRecipeBuild.recipe == self))
86+ store.execute("""
87+ DELETE FROM CharmFile
88+ USING CharmRecipeBuild
89+ WHERE
90+ CharmFile.build = CharmRecipeBuild.id AND
91+ CharmRecipeBuild.recipe = ?
92+ """, (self.id,))
93+ store.find(CharmRecipeBuild, CharmRecipeBuild.recipe == self).remove()
94+ affected_jobs = Select(
95+ [CharmRecipeJob.job_id],
96+ And(CharmRecipeJob.job == Job.id, CharmRecipeJob.recipe == self))
97+ store.find(Job, Job.id.is_in(affected_jobs)).remove()
98+ store.remove(self)
99+ store.find(
100+ BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
101
102
103 @implementer(ICharmRecipeSet)
104@@ -664,7 +699,7 @@ class CharmRecipeSet:
105
106 if git_ref is None:
107 raise NoSourceForCharmRecipe
108- if self.getByName(owner, project, name) is not None:
109+ if self.exists(owner, project, name):
110 raise DuplicateCharmRecipeName
111
112 # The relevant validators will do their own checks as well, but we
113@@ -690,11 +725,21 @@ class CharmRecipeSet:
114
115 return recipe
116
117- def getByName(self, owner, project, name):
118- """See `ICharmRecipeSet`."""
119+ def _getByName(self, owner, project, name):
120 return IStore(CharmRecipe).find(
121 CharmRecipe, owner=owner, project=project, name=name).one()
122
123+ def exists(self, owner, project, name):
124+ """See `ICharmRecipeSet`."""
125+ return self._getByName(owner, project, name) is not None
126+
127+ def getByName(self, owner, project, name):
128+ """See `ICharmRecipeSet`."""
129+ recipe = self._getByName(owner, project, name)
130+ if recipe is None:
131+ raise NoSuchCharmRecipe(name)
132+ return recipe
133+
134 def _getRecipesFromCollection(self, collection, owner=None,
135 visible_by_user=None):
136 id_column = CharmRecipe.git_repository_id
137diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
138index af7f1e3..edd34d2 100644
139--- a/lib/lp/charms/tests/test_charmrecipe.py
140+++ b/lib/lp/charms/tests/test_charmrecipe.py
141@@ -32,6 +32,7 @@ from lp.buildmaster.interfaces.processor import (
142 IProcessorSet,
143 ProcessorNotFound,
144 )
145+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
146 from lp.buildmaster.model.buildqueue import BuildQueue
147 from lp.charms.interfaces.charmrecipe import (
148 BadCharmRecipeSearchContext,
149@@ -46,28 +47,40 @@ from lp.charms.interfaces.charmrecipe import (
150 ICharmRecipeSet,
151 NoSourceForCharmRecipe,
152 )
153-from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
154+from lp.charms.interfaces.charmrecipebuild import (
155+ ICharmRecipeBuild,
156+ ICharmRecipeBuildSet,
157+ )
158 from lp.charms.interfaces.charmrecipejob import (
159 ICharmRecipeRequestBuildsJobSource,
160 )
161+from lp.charms.model.charmrecipebuild import CharmFile
162+from lp.charms.model.charmrecipejob import CharmRecipeJob
163 from lp.code.tests.helpers import GitHostingFixture
164 from lp.registry.interfaces.series import SeriesStatus
165 from lp.services.database.constants import (
166 ONE_DAY_AGO,
167 UTC_NOW,
168 )
169+from lp.services.config import config
170 from lp.services.database.interfaces import IStore
171-from lp.services.database.sqlbase import get_transaction_timestamp
172+from lp.services.database.sqlbase import (
173+ flush_database_caches,
174+ get_transaction_timestamp,
175+ )
176 from lp.services.features.testing import FeatureFixture
177 from lp.services.job.interfaces.job import JobStatus
178+from lp.services.job.runner import JobRunner
179 from lp.services.webapp.snapshot import notify_modified
180 from lp.testing import (
181 admin_logged_in,
182 person_logged_in,
183 TestCaseWithFactory,
184 )
185+from lp.testing.dbuser import dbuser
186 from lp.testing.layers import (
187 DatabaseFunctionalLayer,
188+ LaunchpadFunctionalLayer,
189 LaunchpadZopelessLayer,
190 )
191
192@@ -500,12 +513,95 @@ class TestCharmRecipe(TestCaseWithFactory):
193 project = self.factory.makeProduct()
194 recipe = self.factory.makeCharmRecipe(
195 registrant=owner, owner=owner, project=project, name="condemned")
196- self.assertIsNotNone(
197- getUtility(ICharmRecipeSet).getByName(owner, project, "condemned"))
198+ self.assertTrue(
199+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
200 with person_logged_in(recipe.owner):
201 recipe.destroySelf()
202- self.assertIsNone(
203- getUtility(ICharmRecipeSet).getByName(owner, project, "condemned"))
204+ self.assertFalse(
205+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
206+
207+
208+class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
209+
210+ layer = LaunchpadFunctionalLayer
211+
212+ def setUp(self):
213+ super(TestCharmRecipeDeleteWithBuilds, self).setUp()
214+ self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
215+
216+ def test_delete_with_builds(self):
217+ # A charm recipe with build requests and builds can be deleted.
218+ # Doing so deletes all its build requests, their builds, and their
219+ # files.
220+ owner = self.factory.makePerson()
221+ project = self.factory.makeProduct()
222+ distroseries = self.factory.makeDistroSeries()
223+ processor = self.factory.makeProcessor(supports_virtualized=True)
224+ das = self.factory.makeDistroArchSeries(
225+ distroseries=distroseries, architecturetag=processor.name,
226+ processor=processor)
227+ das.addOrUpdateChroot(
228+ self.factory.makeLibraryFileAlias(
229+ filename="fake_chroot.tar.gz", db_only=True))
230+ self.useFixture(GitHostingFixture(blob=dedent("""\
231+ bases:
232+ - build-on:
233+ - name: "%s"
234+ channel: "%s"
235+ architectures: [%s]
236+ """ % (
237+ distroseries.distribution.name, distroseries.name,
238+ processor.name))))
239+ [git_ref] = self.factory.makeGitRefs()
240+ condemned_recipe = self.factory.makeCharmRecipe(
241+ registrant=owner, owner=owner, project=project, name="condemned",
242+ git_ref=git_ref)
243+ other_recipe = self.factory.makeCharmRecipe(
244+ registrant=owner, owner=owner, project=project, git_ref=git_ref)
245+ self.assertTrue(
246+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
247+ with person_logged_in(owner):
248+ requests = []
249+ jobs = []
250+ for recipe in (condemned_recipe, other_recipe):
251+ requests.append(recipe.requestBuilds(owner))
252+ jobs.append(removeSecurityProxy(requests[-1])._job)
253+ with dbuser(config.ICharmRecipeRequestBuildsJobSource.dbuser):
254+ JobRunner(jobs).runAll()
255+ for job in jobs:
256+ self.assertEqual(JobStatus.COMPLETED, job.job.status)
257+ [build] = requests[0].builds
258+ [other_build] = requests[1].builds
259+ charm_file = self.factory.makeCharmFile(build=build)
260+ other_charm_file = self.factory.makeCharmFile(build=other_build)
261+ store = Store.of(condemned_recipe)
262+ store.flush()
263+ job_ids = [job.job_id for job in jobs]
264+ build_id = build.id
265+ build_queue_id = build.buildqueue_record.id
266+ build_farm_job_id = removeSecurityProxy(build).build_farm_job_id
267+ charm_file_id = removeSecurityProxy(charm_file).id
268+ with person_logged_in(condemned_recipe.owner):
269+ condemned_recipe.destroySelf()
270+ flush_database_caches()
271+ # The deleted recipe, its build requests, and its builds are gone.
272+ self.assertFalse(
273+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
274+ self.assertIsNone(store.get(CharmRecipeJob, job_ids[0]))
275+ self.assertIsNone(getUtility(ICharmRecipeBuildSet).getByID(build_id))
276+ self.assertIsNone(store.get(BuildQueue, build_queue_id))
277+ self.assertIsNone(store.get(BuildFarmJob, build_farm_job_id))
278+ self.assertIsNone(store.get(CharmFile, charm_file_id))
279+ # Unrelated build requests and builds are still present.
280+ self.assertEqual(
281+ removeSecurityProxy(jobs[1]).context,
282+ store.get(CharmRecipeJob, job_ids[1]))
283+ self.assertEqual(
284+ other_build,
285+ getUtility(ICharmRecipeBuildSet).getByID(other_build.id))
286+ self.assertIsNotNone(other_build.buildqueue_record)
287+ self.assertIsNotNone(
288+ store.get(CharmFile, removeSecurityProxy(other_charm_file).id))
289
290
291 class TestCharmRecipeSet(TestCaseWithFactory):

Subscribers

People subscribed via source and target branches

to status/vote changes: