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
=== modified file 'lib/lp/archivepublisher/scripts/generate_contents_files.py'
--- lib/lp/archivepublisher/scripts/generate_contents_files.py 2012-11-10 02:25:07 +0000
+++ lib/lp/archivepublisher/scripts/generate_contents_files.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the1# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Archive Contents files generator."""4"""Archive Contents files generator."""
@@ -16,7 +16,6 @@
16from lp.archivepublisher.config import getPubConfig16from lp.archivepublisher.config import getPubConfig
17from lp.registry.interfaces.distribution import IDistributionSet17from lp.registry.interfaces.distribution import IDistributionSet
18from lp.registry.interfaces.pocket import PackagePublishingPocket18from lp.registry.interfaces.pocket import PackagePublishingPocket
19from lp.registry.interfaces.series import SeriesStatus
20from lp.services.command_spawner import (19from lp.services.command_spawner import (
21 CommandSpawner,20 CommandSpawner,
22 OutputLineHandler,21 OutputLineHandler,
@@ -132,20 +131,9 @@
132 if not file_exists(path):131 if not file_exists(path):
133 os.makedirs(path)132 os.makedirs(path)
134133
135 def getSupportedSeries(self):
136 """Return suites that are supported in this distribution.
137
138 "Supported" means not EXPERIMENTAL or OBSOLETE.
139 """
140 unsupported_status = (SeriesStatus.EXPERIMENTAL,
141 SeriesStatus.OBSOLETE)
142 for series in self.distribution:
143 if series.status not in unsupported_status:
144 yield series
145
146 def getSuites(self):134 def getSuites(self):
147 """Return suites that are actually supported in this distribution."""135 """Return suites that need Contents files."""
148 for series in self.getSupportedSeries():136 for series in self.distribution.getNonObsoleteSeries():
149 for pocket in PackagePublishingPocket.items:137 for pocket in PackagePublishingPocket.items:
150 suite = series.getSuite(pocket)138 suite = series.getSuite(pocket)
151 if file_exists(os.path.join(self.config.distsroot, suite)):139 if file_exists(os.path.join(self.config.distsroot, suite)):
@@ -269,12 +257,12 @@
269 # re-fetch them unnecessarily.257 # re-fetch them unnecessarily.
270 if differ_in_content(current_contents, last_contents):258 if differ_in_content(current_contents, last_contents):
271 self.logger.debug(259 self.logger.debug(
272 "Installing new Contents file for %s/%s.", suite, arch)260 "Staging new Contents file for %s/%s.", suite, arch)
273261
274 new_contents = os.path.join(262 new_contents = os.path.join(
275 contents_dir, "%s.gz" % contents_filename)263 contents_dir, "%s.gz" % contents_filename)
276 contents_dest = os.path.join(264 contents_dest = os.path.join(
277 self.config.distsroot, suite, "%s.gz" % contents_filename)265 contents_dir, "%s-staged.gz" % contents_filename)
278266
279 os.rename(current_contents, last_contents)267 os.rename(current_contents, last_contents)
280 os.rename(new_contents, contents_dest)268 os.rename(new_contents, contents_dest)
281269
=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2014-08-09 19:45:00 +0000
+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the1# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Master distro publishing script."""4"""Master distro publishing script."""
@@ -10,6 +10,7 @@
1010
11from datetime import datetime11from datetime import datetime
12import os12import os
13import shutil
1314
14from pytz import utc15from pytz import utc
15from zope.component import getUtility16from zope.component import getUtility
@@ -156,6 +157,19 @@
156 for purpose, config in candidates if config is not None)157 for purpose, config in candidates if config is not None)
157158
158159
160def newer_mtime(one_file, other_file):
161 """Is one_file newer than other_file, or is other_file missing?"""
162 try:
163 one_mtime = os.stat(one_file).st_mtime
164 except OSError:
165 return False
166 try:
167 other_mtime = os.stat(other_file).st_mtime
168 except OSError:
169 return True
170 return one_mtime > other_mtime
171
172
159class PublishFTPMaster(LaunchpadCronScript):173class PublishFTPMaster(LaunchpadCronScript):
160 """Publish a distro (update).174 """Publish a distro (update).
161175
@@ -539,6 +553,32 @@
539 # most of its time.553 # most of its time.
540 self.publishDistroArchive(distribution, archive)554 self.publishDistroArchive(distribution, archive)
541555
556 def updateContentsFile(self, distribution, suite, arch):
557 """Update a single Contents file if necessary."""
558 config = self.configs[distribution][ArchivePurpose.PRIMARY]
559 backup_dists = get_backup_dists(config)
560 content_dists = os.path.join(
561 config.distroroot, "contents-generation", distribution.name,
562 "dists")
563 contents_filename = "Contents-%s" % arch.architecturetag
564 current_contents = os.path.join(
565 backup_dists, suite, "%s.gz" % contents_filename)
566 new_contents = os.path.join(
567 content_dists, suite, "%s-staged.gz" % contents_filename)
568 if newer_mtime(new_contents, current_contents):
569 self.logger.debug(
570 "Installing new Contents file for %s/%s.", suite,
571 arch.architecturetag)
572 shutil.copy2(new_contents, current_contents)
573
574 def updateContentsFiles(self, distribution):
575 """Pick up updated Contents files if necessary."""
576 for series in distribution.getNonObsoleteSeries():
577 for pocket in PackagePublishingPocket.items:
578 suite = series.getSuite(pocket)
579 for arch in series.enabled_architectures:
580 self.updateContentsFile(distribution, suite, arch)
581
542 def publish(self, distribution, security_only=False):582 def publish(self, distribution, security_only=False):
543 """Do the main publishing work.583 """Do the main publishing work.
544584
@@ -558,6 +598,8 @@
558 # Let's assume the main archive is always modified598 # Let's assume the main archive is always modified
559 has_published = True599 has_published = True
560600
601 self.updateContentsFiles(distribution)
602
561 # Swizzle the now-updated backup dists and the current dists603 # Swizzle the now-updated backup dists and the current dists
562 # around.604 # around.
563 self.installDists(distribution)605 self.installDists(distribution)
564606
=== modified file 'lib/lp/archivepublisher/tests/test_generate_contents_files.py'
--- lib/lp/archivepublisher/tests/test_generate_contents_files.py 2013-09-11 08:17:34 +0000
+++ lib/lp/archivepublisher/tests/test_generate_contents_files.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the1# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test for the `generate-contents-files` script."""4"""Test for the `generate-contents-files` script."""
@@ -15,6 +15,7 @@
15 execute,15 execute,
16 GenerateContentsFiles,16 GenerateContentsFiles,
17 )17 )
18from lp.archivepublisher.scripts.publish_ftpmaster import PublishFTPMaster
18from lp.registry.interfaces.pocket import PackagePublishingPocket19from lp.registry.interfaces.pocket import PackagePublishingPocket
19from lp.services.log.logger import DevNullLogger20from lp.services.log.logger import DevNullLogger
20from lp.services.osutils import write_file21from lp.services.osutils import write_file
@@ -183,14 +184,6 @@
183 self.assertEqual(184 self.assertEqual(
184 [das.architecturetag], script.getArchs(distroseries.name))185 [das.architecturetag], script.getArchs(distroseries.name))
185186
186 def test_getSupportedSeries(self):
187 # getSupportedSeries returns the supported distroseries in the
188 # distribution.
189 script = self.makeScript()
190 distroseries = self.factory.makeDistroSeries(
191 distribution=script.distribution)
192 self.assertIn(distroseries, script.getSupportedSeries())
193
194 def test_getSuites(self):187 def test_getSuites(self):
195 # getSuites returns the full names (distroseries-pocket) of the188 # getSuites returns the full names (distroseries-pocket) of the
196 # pockets that have packages to publish.189 # pockets that have packages to publish.
@@ -268,7 +261,8 @@
268 script.content_archive, StartsWith(script.config.distroroot))261 script.content_archive, StartsWith(script.config.distroroot))
269262
270 def test_main(self):263 def test_main(self):
271 # If run end-to-end, the script generates Contents.gz files.264 # If run end-to-end, the script generates Contents.gz files, and a
265 # following publisher run will put those files in their final place.
272 distro = self.makeDistro()266 distro = self.makeDistro()
273 distroseries = self.factory.makeDistroSeries(distribution=distro)267 distroseries = self.factory.makeDistroSeries(distribution=distro)
274 processor = self.factory.makeProcessor()268 processor = self.factory.makeProcessor()
@@ -287,6 +281,13 @@
287 fake_overrides(script, distroseries)281 fake_overrides(script, distroseries)
288 script.process()282 script.process()
289 self.assertTrue(file_exists(os.path.join(283 self.assertTrue(file_exists(os.path.join(
284 script.content_archive, distro.name, "dists", suite,
285 "Contents-%s-staged.gz" % das.architecturetag)))
286 publisher_script = PublishFTPMaster(test_args=["-d", distro.name])
287 publisher_script.txn = self.layer.txn
288 publisher_script.logger = DevNullLogger()
289 publisher_script.main()
290 self.assertTrue(file_exists(os.path.join(
290 script.config.distsroot, suite,291 script.config.distsroot, suite,
291 "Contents-%s.gz" % das.architecturetag)))292 "Contents-%s.gz" % das.architecturetag)))
292293
293294
=== modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py'
--- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2014-04-15 16:32:03 +0000
+++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the1# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test publish-ftpmaster cron script."""4"""Test publish-ftpmaster cron script."""
@@ -8,6 +8,7 @@
8import logging8import logging
9import os9import os
10from textwrap import dedent10from textwrap import dedent
11import time
1112
12from apt_pkg import TagFile13from apt_pkg import TagFile
13from testtools.matchers import (14from testtools.matchers import (
@@ -24,6 +25,7 @@
24 compose_shell_boolean,25 compose_shell_boolean,
25 find_run_parts_dir,26 find_run_parts_dir,
26 get_working_dists,27 get_working_dists,
28 newer_mtime,
27 PublishFTPMaster,29 PublishFTPMaster,
28 shell_quote,30 shell_quote,
29 )31 )
@@ -38,6 +40,7 @@
38 BufferLogger,40 BufferLogger,
39 DevNullLogger,41 DevNullLogger,
40 )42 )
43from lp.services.osutils import write_file
41from lp.services.scripts.base import LaunchpadScriptFailure44from lp.services.scripts.base import LaunchpadScriptFailure
42from lp.services.utils import file_exists45from lp.services.utils import file_exists
43from lp.soyuz.enums import (46from lp.soyuz.enums import (
@@ -208,6 +211,46 @@
208 self.assertEqual("no", compose_shell_boolean(False))211 self.assertEqual("no", compose_shell_boolean(False))
209212
210213
214class TestNewerMtime(TestCase):
215
216 def setUp(self):
217 super(TestCase, self).setUp()
218 tempdir = self.useTempDir()
219 self.a = os.path.join(tempdir, "a")
220 self.b = os.path.join(tempdir, "b")
221
222 def test_both_missing(self):
223 self.assertFalse(newer_mtime(self.a, self.b))
224
225 def test_one_missing(self):
226 write_file(self.b, "")
227 self.assertFalse(newer_mtime(self.a, self.b))
228
229 def test_other_missing(self):
230 write_file(self.a, "")
231 self.assertTrue(newer_mtime(self.a, self.b))
232
233 def test_older(self):
234 write_file(self.a, "")
235 os.utime(self.a, (0, 0))
236 write_file(self.b, "")
237 self.assertFalse(newer_mtime(self.a, self.b))
238
239 def test_equal(self):
240 now = time.time()
241 write_file(self.a, "")
242 os.utime(self.a, (now, now))
243 write_file(self.b, "")
244 os.utime(self.b, (now, now))
245 self.assertFalse(newer_mtime(self.a, self.b))
246
247 def test_newer(self):
248 write_file(self.a, "")
249 write_file(self.b, "")
250 os.utime(self.b, (0, 0))
251 self.assertTrue(newer_mtime(self.a, self.b))
252
253
211class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin):254class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin):
212 layer = ZopelessDatabaseLayer255 layer = ZopelessDatabaseLayer
213256
@@ -796,6 +839,34 @@
796 "Did not find expected marker for %s."839 "Did not find expected marker for %s."
797 % archive.purpose.title)840 % archive.purpose.title)
798841
842 def test_updateContentsFile_installs_changed(self):
843 distro = self.makeDistroWithPublishDirectory()
844 distroseries = self.factory.makeDistroSeries(distribution=distro)
845 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
846 script = self.makeScript(distro)
847 script.setUp()
848 script.setUpDirs()
849 archive_config = getPubConfig(distro.main_archive)
850 contents_filename = "Contents-%s" % das.architecturetag
851 backup_suite = os.path.join(
852 archive_config.archiveroot + "-distscopy", "dists",
853 distroseries.name)
854 os.makedirs(backup_suite)
855 write_marker_file(
856 [backup_suite, "%s.gz" % contents_filename], "Old Contents")
857 os.utime(
858 os.path.join(backup_suite, "%s.gz" % contents_filename), (0, 0))
859 content_suite = os.path.join(
860 archive_config.distroroot, "contents-generation", distro.name,
861 "dists", distroseries.name)
862 os.makedirs(content_suite)
863 write_marker_file(
864 [content_suite, "%s-staged.gz" % contents_filename], "Contents")
865 script.updateContentsFile(distro, distroseries.name, das)
866 self.assertEqual(
867 "Contents",
868 read_marker_file([backup_suite, "%s.gz" % contents_filename]))
869
799 def test_publish_always_returns_true_for_primary(self):870 def test_publish_always_returns_true_for_primary(self):
800 script = self.makeScript()871 script = self.makeScript()
801 script.publishDistroUploads = FakeMethod()872 script.publishDistroUploads = FakeMethod()
802873
=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py 2014-11-17 18:36:16 +0000
+++ lib/lp/registry/interfaces/distribution.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interfaces including and related to IDistribution."""4"""Interfaces including and related to IDistribution."""
@@ -408,6 +408,9 @@
408 def getDevelopmentSeries():408 def getDevelopmentSeries():
409 """Return the DistroSeries which are marked as in development."""409 """Return the DistroSeries which are marked as in development."""
410410
411 def getNonObsoleteSeries():
412 """Return the non-OBSOLETE DistroSeries in this distribution."""
413
411 def resolveSeriesAlias(name):414 def resolveSeriesAlias(name):
412 """Resolve a series alias.415 """Resolve a series alias.
413416
414417
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2014-12-08 04:20:17 +0000
+++ lib/lp/registry/model/distribution.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes for implementing distribution items."""4"""Database classes for implementing distribution items."""
@@ -785,6 +785,12 @@
785 distribution=self,785 distribution=self,
786 status=SeriesStatus.DEVELOPMENT)786 status=SeriesStatus.DEVELOPMENT)
787787
788 def getNonObsoleteSeries(self):
789 """See `IDistribution`."""
790 for series in self.series:
791 if series.status != SeriesStatus.OBSOLETE:
792 yield series
793
788 def getMilestone(self, name):794 def getMilestone(self, name):
789 """See `IDistribution`."""795 """See `IDistribution`."""
790 return Milestone.selectOne("""796 return Milestone.selectOne("""
791797
=== modified file 'lib/lp/registry/tests/test_distribution.py'
--- lib/lp/registry/tests/test_distribution.py 2013-08-01 14:09:45 +0000
+++ lib/lp/registry/tests/test_distribution.py 2015-02-13 10:45:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for Distribution."""4"""Tests for Distribution."""
@@ -383,7 +383,7 @@
383383
384384
385class SeriesTests(TestCaseWithFactory):385class SeriesTests(TestCaseWithFactory):
386 """Test IDistribution.getSeries().386 """Test IDistribution.getSeries() and friends.
387 """387 """
388388
389 layer = LaunchpadFunctionalLayer389 layer = LaunchpadFunctionalLayer
@@ -416,6 +416,20 @@
416 self.assertEqual(416 self.assertEqual(
417 series, distro.getSeries("devel", follow_aliases=True))417 series, distro.getSeries("devel", follow_aliases=True))
418418
419 def test_getNonObsoleteSeries(self):
420 distro = self.factory.makeDistribution()
421 self.factory.makeDistroSeries(
422 distribution=distro, status=SeriesStatus.OBSOLETE)
423 current = self.factory.makeDistroSeries(
424 distribution=distro, status=SeriesStatus.CURRENT)
425 development = self.factory.makeDistroSeries(
426 distribution=distro, status=SeriesStatus.DEVELOPMENT)
427 experimental = self.factory.makeDistroSeries(
428 distribution=distro, status=SeriesStatus.EXPERIMENTAL)
429 self.assertContentEqual(
430 [current, development, experimental],
431 list(distro.getNonObsoleteSeries()))
432
419433
420class DerivativesTests(TestCaseWithFactory):434class DerivativesTests(TestCaseWithFactory):
421 """Test IDistribution.derivatives.435 """Test IDistribution.derivatives.