Merge lp:~cjwatson/launchpad/contents-race into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 17341
Proposed branch: lp:~cjwatson/launchpad/contents-race
Merge into: lp:launchpad
Diff against target: 391 lines (+158/-33)
7 files modified
lib/lp/archivepublisher/scripts/generate_contents_files.py (+5/-17)
lib/lp/archivepublisher/scripts/publish_ftpmaster.py (+43/-1)
lib/lp/archivepublisher/tests/test_generate_contents_files.py (+11/-10)
lib/lp/archivepublisher/tests/test_publish_ftpmaster.py (+72/-1)
lib/lp/registry/interfaces/distribution.py (+4/-1)
lib/lp/registry/model/distribution.py (+7/-1)
lib/lp/registry/tests/test_distribution.py (+16/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/contents-race
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+249547@code.launchpad.net

Commit message

Update Contents files in a way that doesn't suffer from races between generate-contents-files and publish-ftpmaster.

Description of the change

Update Contents files in a way that doesn't suffer from races between generate-contents-files and publish-ftpmaster.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

getSupportedSeries is probably not an ideal name, given that SeriesStatus.SUPPORTED exists.

Also, we might want to make the method itself less nonsensical. Experimental is an active series status, so it probably wants Contents files.

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_contents_files.py'
2--- lib/lp/archivepublisher/scripts/generate_contents_files.py 2012-11-10 02:25:07 +0000
3+++ lib/lp/archivepublisher/scripts/generate_contents_files.py 2015-02-13 10:45:35 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
6+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Archive Contents files generator."""
10@@ -16,7 +16,6 @@
11 from lp.archivepublisher.config import getPubConfig
12 from lp.registry.interfaces.distribution import IDistributionSet
13 from lp.registry.interfaces.pocket import PackagePublishingPocket
14-from lp.registry.interfaces.series import SeriesStatus
15 from lp.services.command_spawner import (
16 CommandSpawner,
17 OutputLineHandler,
18@@ -132,20 +131,9 @@
19 if not file_exists(path):
20 os.makedirs(path)
21
22- def getSupportedSeries(self):
23- """Return suites that are supported in this distribution.
24-
25- "Supported" means not EXPERIMENTAL or OBSOLETE.
26- """
27- unsupported_status = (SeriesStatus.EXPERIMENTAL,
28- SeriesStatus.OBSOLETE)
29- for series in self.distribution:
30- if series.status not in unsupported_status:
31- yield series
32-
33 def getSuites(self):
34- """Return suites that are actually supported in this distribution."""
35- for series in self.getSupportedSeries():
36+ """Return suites that need Contents files."""
37+ for series in self.distribution.getNonObsoleteSeries():
38 for pocket in PackagePublishingPocket.items:
39 suite = series.getSuite(pocket)
40 if file_exists(os.path.join(self.config.distsroot, suite)):
41@@ -269,12 +257,12 @@
42 # re-fetch them unnecessarily.
43 if differ_in_content(current_contents, last_contents):
44 self.logger.debug(
45- "Installing new Contents file for %s/%s.", suite, arch)
46+ "Staging new Contents file for %s/%s.", suite, arch)
47
48 new_contents = os.path.join(
49 contents_dir, "%s.gz" % contents_filename)
50 contents_dest = os.path.join(
51- self.config.distsroot, suite, "%s.gz" % contents_filename)
52+ contents_dir, "%s-staged.gz" % contents_filename)
53
54 os.rename(current_contents, last_contents)
55 os.rename(new_contents, contents_dest)
56
57=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
58--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2014-08-09 19:45:00 +0000
59+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2015-02-13 10:45:35 +0000
60@@ -1,4 +1,4 @@
61-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
62+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
63 # GNU Affero General Public License version 3 (see the file LICENSE).
64
65 """Master distro publishing script."""
66@@ -10,6 +10,7 @@
67
68 from datetime import datetime
69 import os
70+import shutil
71
72 from pytz import utc
73 from zope.component import getUtility
74@@ -156,6 +157,19 @@
75 for purpose, config in candidates if config is not None)
76
77
78+def newer_mtime(one_file, other_file):
79+ """Is one_file newer than other_file, or is other_file missing?"""
80+ try:
81+ one_mtime = os.stat(one_file).st_mtime
82+ except OSError:
83+ return False
84+ try:
85+ other_mtime = os.stat(other_file).st_mtime
86+ except OSError:
87+ return True
88+ return one_mtime > other_mtime
89+
90+
91 class PublishFTPMaster(LaunchpadCronScript):
92 """Publish a distro (update).
93
94@@ -539,6 +553,32 @@
95 # most of its time.
96 self.publishDistroArchive(distribution, archive)
97
98+ def updateContentsFile(self, distribution, suite, arch):
99+ """Update a single Contents file if necessary."""
100+ config = self.configs[distribution][ArchivePurpose.PRIMARY]
101+ backup_dists = get_backup_dists(config)
102+ content_dists = os.path.join(
103+ config.distroroot, "contents-generation", distribution.name,
104+ "dists")
105+ contents_filename = "Contents-%s" % arch.architecturetag
106+ current_contents = os.path.join(
107+ backup_dists, suite, "%s.gz" % contents_filename)
108+ new_contents = os.path.join(
109+ content_dists, suite, "%s-staged.gz" % contents_filename)
110+ if newer_mtime(new_contents, current_contents):
111+ self.logger.debug(
112+ "Installing new Contents file for %s/%s.", suite,
113+ arch.architecturetag)
114+ shutil.copy2(new_contents, current_contents)
115+
116+ def updateContentsFiles(self, distribution):
117+ """Pick up updated Contents files if necessary."""
118+ for series in distribution.getNonObsoleteSeries():
119+ for pocket in PackagePublishingPocket.items:
120+ suite = series.getSuite(pocket)
121+ for arch in series.enabled_architectures:
122+ self.updateContentsFile(distribution, suite, arch)
123+
124 def publish(self, distribution, security_only=False):
125 """Do the main publishing work.
126
127@@ -558,6 +598,8 @@
128 # Let's assume the main archive is always modified
129 has_published = True
130
131+ self.updateContentsFiles(distribution)
132+
133 # Swizzle the now-updated backup dists and the current dists
134 # around.
135 self.installDists(distribution)
136
137=== modified file 'lib/lp/archivepublisher/tests/test_generate_contents_files.py'
138--- lib/lp/archivepublisher/tests/test_generate_contents_files.py 2013-09-11 08:17:34 +0000
139+++ lib/lp/archivepublisher/tests/test_generate_contents_files.py 2015-02-13 10:45:35 +0000
140@@ -1,4 +1,4 @@
141-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
142+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
143 # GNU Affero General Public License version 3 (see the file LICENSE).
144
145 """Test for the `generate-contents-files` script."""
146@@ -15,6 +15,7 @@
147 execute,
148 GenerateContentsFiles,
149 )
150+from lp.archivepublisher.scripts.publish_ftpmaster import PublishFTPMaster
151 from lp.registry.interfaces.pocket import PackagePublishingPocket
152 from lp.services.log.logger import DevNullLogger
153 from lp.services.osutils import write_file
154@@ -183,14 +184,6 @@
155 self.assertEqual(
156 [das.architecturetag], script.getArchs(distroseries.name))
157
158- def test_getSupportedSeries(self):
159- # getSupportedSeries returns the supported distroseries in the
160- # distribution.
161- script = self.makeScript()
162- distroseries = self.factory.makeDistroSeries(
163- distribution=script.distribution)
164- self.assertIn(distroseries, script.getSupportedSeries())
165-
166 def test_getSuites(self):
167 # getSuites returns the full names (distroseries-pocket) of the
168 # pockets that have packages to publish.
169@@ -268,7 +261,8 @@
170 script.content_archive, StartsWith(script.config.distroroot))
171
172 def test_main(self):
173- # If run end-to-end, the script generates Contents.gz files.
174+ # If run end-to-end, the script generates Contents.gz files, and a
175+ # following publisher run will put those files in their final place.
176 distro = self.makeDistro()
177 distroseries = self.factory.makeDistroSeries(distribution=distro)
178 processor = self.factory.makeProcessor()
179@@ -287,6 +281,13 @@
180 fake_overrides(script, distroseries)
181 script.process()
182 self.assertTrue(file_exists(os.path.join(
183+ script.content_archive, distro.name, "dists", suite,
184+ "Contents-%s-staged.gz" % das.architecturetag)))
185+ publisher_script = PublishFTPMaster(test_args=["-d", distro.name])
186+ publisher_script.txn = self.layer.txn
187+ publisher_script.logger = DevNullLogger()
188+ publisher_script.main()
189+ self.assertTrue(file_exists(os.path.join(
190 script.config.distsroot, suite,
191 "Contents-%s.gz" % das.architecturetag)))
192
193
194=== modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py'
195--- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2014-04-15 16:32:03 +0000
196+++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2015-02-13 10:45:35 +0000
197@@ -1,4 +1,4 @@
198-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
199+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
200 # GNU Affero General Public License version 3 (see the file LICENSE).
201
202 """Test publish-ftpmaster cron script."""
203@@ -8,6 +8,7 @@
204 import logging
205 import os
206 from textwrap import dedent
207+import time
208
209 from apt_pkg import TagFile
210 from testtools.matchers import (
211@@ -24,6 +25,7 @@
212 compose_shell_boolean,
213 find_run_parts_dir,
214 get_working_dists,
215+ newer_mtime,
216 PublishFTPMaster,
217 shell_quote,
218 )
219@@ -38,6 +40,7 @@
220 BufferLogger,
221 DevNullLogger,
222 )
223+from lp.services.osutils import write_file
224 from lp.services.scripts.base import LaunchpadScriptFailure
225 from lp.services.utils import file_exists
226 from lp.soyuz.enums import (
227@@ -208,6 +211,46 @@
228 self.assertEqual("no", compose_shell_boolean(False))
229
230
231+class TestNewerMtime(TestCase):
232+
233+ def setUp(self):
234+ super(TestCase, self).setUp()
235+ tempdir = self.useTempDir()
236+ self.a = os.path.join(tempdir, "a")
237+ self.b = os.path.join(tempdir, "b")
238+
239+ def test_both_missing(self):
240+ self.assertFalse(newer_mtime(self.a, self.b))
241+
242+ def test_one_missing(self):
243+ write_file(self.b, "")
244+ self.assertFalse(newer_mtime(self.a, self.b))
245+
246+ def test_other_missing(self):
247+ write_file(self.a, "")
248+ self.assertTrue(newer_mtime(self.a, self.b))
249+
250+ def test_older(self):
251+ write_file(self.a, "")
252+ os.utime(self.a, (0, 0))
253+ write_file(self.b, "")
254+ self.assertFalse(newer_mtime(self.a, self.b))
255+
256+ def test_equal(self):
257+ now = time.time()
258+ write_file(self.a, "")
259+ os.utime(self.a, (now, now))
260+ write_file(self.b, "")
261+ os.utime(self.b, (now, now))
262+ self.assertFalse(newer_mtime(self.a, self.b))
263+
264+ def test_newer(self):
265+ write_file(self.a, "")
266+ write_file(self.b, "")
267+ os.utime(self.b, (0, 0))
268+ self.assertTrue(newer_mtime(self.a, self.b))
269+
270+
271 class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin):
272 layer = ZopelessDatabaseLayer
273
274@@ -796,6 +839,34 @@
275 "Did not find expected marker for %s."
276 % archive.purpose.title)
277
278+ def test_updateContentsFile_installs_changed(self):
279+ distro = self.makeDistroWithPublishDirectory()
280+ distroseries = self.factory.makeDistroSeries(distribution=distro)
281+ das = self.factory.makeDistroArchSeries(distroseries=distroseries)
282+ script = self.makeScript(distro)
283+ script.setUp()
284+ script.setUpDirs()
285+ archive_config = getPubConfig(distro.main_archive)
286+ contents_filename = "Contents-%s" % das.architecturetag
287+ backup_suite = os.path.join(
288+ archive_config.archiveroot + "-distscopy", "dists",
289+ distroseries.name)
290+ os.makedirs(backup_suite)
291+ write_marker_file(
292+ [backup_suite, "%s.gz" % contents_filename], "Old Contents")
293+ os.utime(
294+ os.path.join(backup_suite, "%s.gz" % contents_filename), (0, 0))
295+ content_suite = os.path.join(
296+ archive_config.distroroot, "contents-generation", distro.name,
297+ "dists", distroseries.name)
298+ os.makedirs(content_suite)
299+ write_marker_file(
300+ [content_suite, "%s-staged.gz" % contents_filename], "Contents")
301+ script.updateContentsFile(distro, distroseries.name, das)
302+ self.assertEqual(
303+ "Contents",
304+ read_marker_file([backup_suite, "%s.gz" % contents_filename]))
305+
306 def test_publish_always_returns_true_for_primary(self):
307 script = self.makeScript()
308 script.publishDistroUploads = FakeMethod()
309
310=== modified file 'lib/lp/registry/interfaces/distribution.py'
311--- lib/lp/registry/interfaces/distribution.py 2014-11-17 18:36:16 +0000
312+++ lib/lp/registry/interfaces/distribution.py 2015-02-13 10:45:35 +0000
313@@ -1,4 +1,4 @@
314-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
315+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
316 # GNU Affero General Public License version 3 (see the file LICENSE).
317
318 """Interfaces including and related to IDistribution."""
319@@ -408,6 +408,9 @@
320 def getDevelopmentSeries():
321 """Return the DistroSeries which are marked as in development."""
322
323+ def getNonObsoleteSeries():
324+ """Return the non-OBSOLETE DistroSeries in this distribution."""
325+
326 def resolveSeriesAlias(name):
327 """Resolve a series alias.
328
329
330=== modified file 'lib/lp/registry/model/distribution.py'
331--- lib/lp/registry/model/distribution.py 2014-12-08 04:20:17 +0000
332+++ lib/lp/registry/model/distribution.py 2015-02-13 10:45:35 +0000
333@@ -1,4 +1,4 @@
334-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
335+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
336 # GNU Affero General Public License version 3 (see the file LICENSE).
337
338 """Database classes for implementing distribution items."""
339@@ -785,6 +785,12 @@
340 distribution=self,
341 status=SeriesStatus.DEVELOPMENT)
342
343+ def getNonObsoleteSeries(self):
344+ """See `IDistribution`."""
345+ for series in self.series:
346+ if series.status != SeriesStatus.OBSOLETE:
347+ yield series
348+
349 def getMilestone(self, name):
350 """See `IDistribution`."""
351 return Milestone.selectOne("""
352
353=== modified file 'lib/lp/registry/tests/test_distribution.py'
354--- lib/lp/registry/tests/test_distribution.py 2013-08-01 14:09:45 +0000
355+++ lib/lp/registry/tests/test_distribution.py 2015-02-13 10:45:35 +0000
356@@ -1,4 +1,4 @@
357-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
358+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
359 # GNU Affero General Public License version 3 (see the file LICENSE).
360
361 """Tests for Distribution."""
362@@ -383,7 +383,7 @@
363
364
365 class SeriesTests(TestCaseWithFactory):
366- """Test IDistribution.getSeries().
367+ """Test IDistribution.getSeries() and friends.
368 """
369
370 layer = LaunchpadFunctionalLayer
371@@ -416,6 +416,20 @@
372 self.assertEqual(
373 series, distro.getSeries("devel", follow_aliases=True))
374
375+ def test_getNonObsoleteSeries(self):
376+ distro = self.factory.makeDistribution()
377+ self.factory.makeDistroSeries(
378+ distribution=distro, status=SeriesStatus.OBSOLETE)
379+ current = self.factory.makeDistroSeries(
380+ distribution=distro, status=SeriesStatus.CURRENT)
381+ development = self.factory.makeDistroSeries(
382+ distribution=distro, status=SeriesStatus.DEVELOPMENT)
383+ experimental = self.factory.makeDistroSeries(
384+ distribution=distro, status=SeriesStatus.EXPERIMENTAL)
385+ self.assertContentEqual(
386+ [current, development, experimental],
387+ list(distro.getNonObsoleteSeries()))
388+
389
390 class DerivativesTests(TestCaseWithFactory):
391 """Test IDistribution.derivatives.