Merge lp:~cjwatson/launchpad/germinate-all-dev-series into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 14624
Proposed branch: lp:~cjwatson/launchpad/germinate-all-dev-series
Merge into: lp:launchpad
Diff against target: 348 lines (+162/-47)
2 files modified
lib/lp/archivepublisher/scripts/generate_extra_overrides.py (+58/-38)
lib/lp/archivepublisher/tests/test_generate_extra_overrides.py (+104/-9)
To merge this branch: bzr merge lp:~cjwatson/launchpad/germinate-all-dev-series
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+86909@code.launchpad.net

Commit message

[r=wgrant][bug=904538] Let cron.germinate handle multiple dev series.

Description of the change

== Summary ==

My recent refactoring of cron.germinate caused some dogfood-specific problems, because dogfood has multiple Ubuntu distroseries in the DEVELOPMENT or FROZEN states.

== Proposed fix ==

Generate extra overrides for all DEVELOPMENT and FROZEN series, rather than for just one. Log but otherwise ignore any cases where missing seeds would previously have caused us to abort.

This seems like logically the right thing to do, and removes the obstacle to testing this easily on dogfood.

== Implementation details ==

I introduced a simple new CachedDistroSeries class to preserve the current property that most of generate-extra-overrides runs without a database transaction, without having to pass around more clumsy piles of arguments (otherwise, I was going to have to add a list of lists of components somewhere).

== Tests ==

bin/test -vvct generate_extra_overrides

== Demo and Q/A ==

Run cron.germinate on mawson. Given the current state of dogfood's database, it should emit log entries about the lack of seeds for rusty, and generate updated extra override files (in /srv/launchpad.net/ubuntu-archive/ubuntu-misc/more-extra.override.{oneiric,precise}.main) for precise and oneiric. Previously, it would fail due to the lack of seeds for rusty.

== lint ==

None.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

It would be better I think to cache those values in the main
DistroSeries, using e.g. @cachedproperty. Its a little more work to
validate, but centralises the caching.

Revision history for this message
Colin Watson (cjwatson) wrote :

Would it be better to just go back to having multiple lists and
iterating over them with zip() (my personal threshold for when I start
thinking about consolidating this kind of thing into a list of classes
seems to be about three, which is why I did it at this point), or is it
worth caching the property for other reasons anyway?

Revision history for this message
Colin Watson (cjwatson) wrote :

I played with @cachedproperty and friends a bit. I eventually decided that I wasn't massively keen on relying on the generational cache for correctness rather than just performance, and switching to the stupid cache started to seem like overengineering for the problem at hand. Instead, I just dropped the CachedDistroSeries business and fetched the relevant properties inside the loop over self.series, so we end up with one transaction per series; this should be fine and is IMO much more obviously correct at sight, which I think is valuable.

The point of the DatabaseBlockedPolicy was just to avoid holding a transaction open while doing lengthy computations. Doing a few queries at the start of the loop body for each series shouldn't be a problem; and in any case that loop will only iterate over a single series in production.

Revision history for this message
William Grant (wgrant) :
review: Approve (codwe)
Revision history for this message
William Grant (wgrant) :
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/scripts/generate_extra_overrides.py'
2--- lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2012-01-03 01:35:27 +0000
4@@ -17,7 +17,10 @@
5 from germinate.archive import TagFile
6 from germinate.germinator import Germinator
7 from germinate.log import GerminateFormatter
8-from germinate.seeds import SeedStructure
9+from germinate.seeds import (
10+ SeedError,
11+ SeedStructure,
12+ )
13 from zope.component import getUtility
14
15 from lp.archivepublisher.config import getPubConfig
16@@ -51,16 +54,14 @@
17
18
19 def find_operable_series(distribution):
20- """Find a series we can operate on in this distribution.
21+ """Find all the series we can operate on in this distribution.
22
23 We are allowed to modify DEVELOPMENT or FROZEN series, but should leave
24 series with any other status alone.
25 """
26- series = distribution.currentseries
27- if series.status in (SeriesStatus.DEVELOPMENT, SeriesStatus.FROZEN):
28- return series
29- else:
30- return None
31+ return [
32+ series for series in distribution.series
33+ if series.status in (SeriesStatus.DEVELOPMENT, SeriesStatus.FROZEN)]
34
35
36 class GenerateExtraOverrides(LaunchpadScript):
37@@ -96,17 +97,11 @@
38 "Distribution '%s' not found." % self.options.distribution)
39
40 self.series = find_operable_series(self.distribution)
41- if self.series is None:
42+ if not self.series:
43 raise LaunchpadScriptFailure(
44- "There is no DEVELOPMENT distroseries for %s." %
45+ "There is no DEVELOPMENT or FROZEN distroseries for %s." %
46 self.options.distribution)
47
48- # Even if DistroSeries.component_names starts including partner, we
49- # don't want it; this applies to the primary archive only.
50- self.components = [component
51- for component in self.series.component_names
52- if component != "partner"]
53-
54 def getConfig(self):
55 """Set up a configuration object for this archive."""
56 archive = self.distribution.main_archive
57@@ -148,11 +143,32 @@
58 self.setUpDirs()
59 self.addLogHandler()
60
61+ def getComponents(self, series):
62+ """Get the list of components to process for a given distroseries.
63+
64+ Even if DistroSeries.component_names starts including partner,
65+ we don't want it; this applies to the primary archive only.
66+ """
67+ return [component
68+ for component in series.component_names
69+ if component != "partner"]
70+
71 def makeSeedStructures(self, series_name, flavours, seed_bases=None):
72 structures = {}
73 for flavour in flavours:
74- structures[flavour] = SeedStructure(
75- "%s.%s" % (flavour, series_name), seed_bases=seed_bases)
76+ try:
77+ structure = SeedStructure(
78+ "%s.%s" % (flavour, series_name), seed_bases=seed_bases)
79+ if len(structure):
80+ structures[flavour] = structure
81+ else:
82+ self.logger.warning(
83+ "Skipping empty seed structure for %s.%s",
84+ flavour, series_name)
85+ except SeedError, e:
86+ self.logger.warning(
87+ "Failed to fetch seeds for %s.%s: %s",
88+ flavour, series_name, e)
89 return structures
90
91 def logGerminateProgress(self, *args):
92@@ -278,14 +294,14 @@
93 if "build-essential" in structure.names and primary_flavour:
94 write_overrides("build-essential", "Build-Essential", "yes")
95
96- def germinateArch(self, override_file, series_name, arch, flavours,
97- structures):
98+ def germinateArch(self, override_file, series_name, components, arch,
99+ flavours, structures):
100 """Germinate seeds on all flavours for a single architecture."""
101 germinator = Germinator(arch)
102
103 # Read archive metadata.
104 archive = TagFile(
105- series_name, self.components, arch,
106+ series_name, components, arch,
107 "file://%s" % self.config.archiveroot, cleanup=True)
108 germinator.parse_archive(archive)
109
110@@ -302,34 +318,38 @@
111 override_file, germinator, series_name, arch, flavour,
112 structures[flavour], flavour == flavours[0])
113
114- def generateExtraOverrides(self, series_name, series_architectures,
115+ def generateExtraOverrides(self, series_name, components, architectures,
116 flavours, seed_bases=None):
117 structures = self.makeSeedStructures(
118 series_name, flavours, seed_bases=seed_bases)
119
120- override_path = os.path.join(
121- self.config.miscroot,
122- "more-extra.override.%s.main" % series_name)
123- with AtomicFile(override_path) as override_file:
124- for arch in series_architectures:
125- self.germinateArch(
126- override_file, series_name, arch, flavours, structures)
127+ if structures:
128+ override_path = os.path.join(
129+ self.config.miscroot,
130+ "more-extra.override.%s.main" % series_name)
131+ with AtomicFile(override_path) as override_file:
132+ for arch in architectures:
133+ self.germinateArch(
134+ override_file, series_name, components, arch,
135+ flavours, structures)
136
137 def process(self, seed_bases=None):
138 """Do the bulk of the work."""
139 self.setUp()
140
141- series_name = self.series.name
142- series_architectures = sorted(
143- [arch.architecturetag for arch in self.series.architectures])
144+ for series in self.series:
145+ series_name = series.name
146+ components = self.getComponents(series)
147+ architectures = sorted(
148+ [arch.architecturetag for arch in series.architectures])
149
150- # This takes a while. Ensure that we do it without keeping a
151- # database transaction open.
152- self.txn.commit()
153- with DatabaseBlockedPolicy():
154- self.generateExtraOverrides(
155- series_name, series_architectures, self.args,
156- seed_bases=seed_bases)
157+ # This takes a while. Ensure that we do it without keeping a
158+ # database transaction open.
159+ self.txn.commit()
160+ with DatabaseBlockedPolicy():
161+ self.generateExtraOverrides(
162+ series_name, components, architectures, self.args,
163+ seed_bases=seed_bases)
164
165 def main(self):
166 """See `LaunchpadScript`."""
167
168=== modified file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py'
169--- lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2012-01-01 02:58:52 +0000
170+++ lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2012-01-03 01:35:27 +0000
171@@ -260,7 +260,8 @@
172 development_distroseries = self.factory.makeDistroSeries(
173 distro, status=SeriesStatus.DEVELOPMENT)
174 script = self.makeScript(distro)
175- self.assertEqual(development_distroseries, script.series)
176+ observed_series = [series.name for series in script.series]
177+ self.assertEqual([development_distroseries.name], observed_series)
178
179 def test_permits_frozen_distro_series(self):
180 # If there is no DEVELOPMENT series, a FROZEN one will do.
181@@ -270,7 +271,8 @@
182 frozen_distroseries = self.factory.makeDistroSeries(
183 distro, status=SeriesStatus.FROZEN)
184 script = self.makeScript(distro)
185- self.assertEqual(frozen_distroseries, script.series)
186+ observed_series = [series.name for series in script.series]
187+ self.assertEqual([frozen_distroseries.name], observed_series)
188
189 def test_requires_development_frozen_distro_series(self):
190 # If there is no DEVELOPMENT or FROZEN series, the script fails.
191@@ -280,6 +282,28 @@
192 script = self.makeScript(distro, run_setup=False)
193 self.assertRaises(LaunchpadScriptFailure, script.processOptions)
194
195+ def test_multiple_development_frozen_distro_series(self):
196+ # If there are multiple DEVELOPMENT or FROZEN series, they are all
197+ # used.
198+ distro = self.makeDistro()
199+ development_distroseries_one = self.factory.makeDistroSeries(
200+ distro, status=SeriesStatus.DEVELOPMENT)
201+ development_distroseries_two = self.factory.makeDistroSeries(
202+ distro, status=SeriesStatus.DEVELOPMENT)
203+ frozen_distroseries_one = self.factory.makeDistroSeries(
204+ distro, status=SeriesStatus.FROZEN)
205+ frozen_distroseries_two = self.factory.makeDistroSeries(
206+ distro, status=SeriesStatus.FROZEN)
207+ script = self.makeScript(distro)
208+ expected_series = [
209+ development_distroseries_one.name,
210+ development_distroseries_two.name,
211+ frozen_distroseries_one.name,
212+ frozen_distroseries_two.name,
213+ ]
214+ observed_series = [series.name for series in script.series]
215+ self.assertContentEqual(expected_series, observed_series)
216+
217 def test_components_exclude_partner(self):
218 # If a 'partner' component exists, it is excluded.
219 distro = self.makeDistro()
220@@ -289,7 +313,8 @@
221 self.factory.makeComponentSelection(
222 distroseries=distroseries, component="partner")
223 script = self.makeScript(distro)
224- self.assertEqual(["main"], script.components)
225+ self.assertEqual(1, len(script.series))
226+ self.assertEqual(["main"], script.getComponents(script.series[0]))
227
228 def test_compose_output_path_in_germinateroot(self):
229 # Output files are written to the correct locations under
230@@ -308,15 +333,58 @@
231 arch),
232 output)
233
234- def fetchGerminatedOverrides(self, script, series_name, arch, flavours):
235+ def test_make_seed_structures_missing_seeds(self):
236+ # makeSeedStructures ignores missing seeds.
237+ distro = self.makeDistro()
238+ distroseries = self.factory.makeDistroSeries(distribution=distro)
239+ series_name = distroseries.name
240+ script = self.makeScript(distro)
241+ flavour = self.factory.getUniqueString()
242+
243+ structures = script.makeSeedStructures(
244+ series_name, [flavour], seed_bases=["file://%s" % self.seeddir])
245+ self.assertEqual({}, structures)
246+
247+ def test_make_seed_structures_empty_seed_structure(self):
248+ # makeSeedStructures ignores an empty seed structure.
249+ distro = self.makeDistro()
250+ distroseries = self.factory.makeDistroSeries(distribution=distro)
251+ series_name = distroseries.name
252+ script = self.makeScript(distro)
253+ flavour = self.factory.getUniqueString()
254+ self.makeSeedStructure(flavour, series_name, [])
255+
256+ structures = script.makeSeedStructures(
257+ series_name, [flavour], seed_bases=["file://%s" % self.seeddir])
258+ self.assertEqual({}, structures)
259+
260+ def test_make_seed_structures_valid_seeds(self):
261+ # makeSeedStructures reads valid seeds successfully.
262+ distro = self.makeDistro()
263+ distroseries = self.factory.makeDistroSeries(distribution=distro)
264+ series_name = distroseries.name
265+ script = self.makeScript(distro)
266+ flavour = self.factory.getUniqueString()
267+ seed = self.factory.getUniqueString()
268+ self.makeSeedStructure(flavour, series_name, [seed])
269+ self.makeSeed(flavour, series_name, seed, [])
270+
271+ structures = script.makeSeedStructures(
272+ series_name, [flavour], seed_bases=["file://%s" % self.seeddir])
273+ self.assertIn(flavour, structures)
274+
275+ def fetchGerminatedOverrides(self, script, distroseries, arch, flavours):
276 """Helper to call script.germinateArch and return overrides."""
277 structures = script.makeSeedStructures(
278- series_name, flavours, seed_bases=["file://%s" % self.seeddir])
279+ distroseries.name, flavours,
280+ seed_bases=["file://%s" % self.seeddir])
281
282 override_fd, override_path = tempfile.mkstemp()
283 with os.fdopen(override_fd, "w") as override_file:
284 script.germinateArch(
285- override_file, series_name, arch, flavours, structures)
286+ override_file, distroseries.name,
287+ script.getComponents(distroseries), arch, flavours,
288+ structures)
289 return file_contents(override_path).splitlines()
290
291 def test_germinate_output(self):
292@@ -344,7 +412,7 @@
293 self.makeSeed(flavour_two, series_name, seed, [two.name])
294
295 overrides = self.fetchGerminatedOverrides(
296- script, series_name, arch, [flavour_one, flavour_two])
297+ script, distroseries, arch, [flavour_one, flavour_two])
298 self.assertEqual([], overrides)
299
300 seed_dir_one = os.path.join(
301@@ -402,7 +470,7 @@
302 headers=["Task-Description: two"])
303
304 overrides = self.fetchGerminatedOverrides(
305- script, series_name, arch, [flavour])
306+ script, distroseries, arch, [flavour])
307 expected_overrides = [
308 "%s/%s Task %s" % (one.name, arch, seed_one),
309 "%s/%s Task %s" % (two.name, arch, seed_one),
310@@ -510,10 +578,37 @@
311 self.makeSeed(flavour, series_name, seed, [package.name])
312
313 overrides = self.fetchGerminatedOverrides(
314- script, series_name, arch, [flavour])
315+ script, distroseries, arch, [flavour])
316 self.assertContentEqual(
317 ["%s/%s Build-Essential yes" % (package.name, arch)], overrides)
318
319+ def test_process_missing_seeds(self):
320+ # The script ignores series with no seed structures.
321+ distro = self.makeDistro()
322+ distroseries_one = self.factory.makeDistroSeries(distribution=distro)
323+ distroseries_two = self.factory.makeDistroSeries(distribution=distro)
324+ component = self.factory.makeComponent()
325+ self.factory.makeComponentSelection(
326+ distroseries=distroseries_one, component=component)
327+ self.factory.makeComponentSelection(
328+ distroseries=distroseries_two, component=component)
329+ self.factory.makeDistroArchSeries(distroseries=distroseries_one)
330+ self.factory.makeDistroArchSeries(distroseries=distroseries_two)
331+ flavour = self.factory.getUniqueString()
332+ script = self.makeScript(distro, extra_args=[flavour])
333+ self.makeIndexFiles(script, distroseries_two)
334+ seed = self.factory.getUniqueString()
335+ self.makeSeedStructure(flavour, distroseries_two.name, [seed])
336+ self.makeSeed(flavour, distroseries_two.name, seed, [])
337+
338+ script.process(seed_bases=["file://%s" % self.seeddir])
339+ self.assertFalse(os.path.exists(os.path.join(
340+ script.config.miscroot,
341+ "more-extra.override.%s.main" % distroseries_one.name)))
342+ self.assertTrue(os.path.exists(os.path.join(
343+ script.config.miscroot,
344+ "more-extra.override.%s.main" % distroseries_two.name)))
345+
346 def test_main(self):
347 # If run end-to-end, the script generates override files containing
348 # output for all architectures, and sends germinate's log output to