Repository: fedora-infra/bodhi

Issue:
Prioritize stable repos when pushing
Currently in bodhi2,1 thread per tag gets kicked off, but we should ensure that stable repos go out first, then testing.

Prioritize stable repos when pushing
Currently in bodhi2,1 thread per tag gets kicked off, but we should ensure that stable repos go out first, then testing.



Discussion:

Resolving patch:
diff --git a/bodhi/masher.py b/bodhi/masher.py
--- a/bodhi/masher.py
+++ b/bodhi/masher.py
@@ -93,23 +93,57 @@ def consume(self, msg):
                 # TODO: send email notifications
                 return
         with self.db_factory() as session:
-            self.work(session, msg)
+            try:
+                self.work(session, msg)
+            except:
+                log.exception('Problem in Masher.work')
+
+    def prioritize_updates(self, releases):
+        """Return 2 batches of repos: important, and normal.
+
+        At the moment an important repo is one that contains a security update.
+        """
+        important, normal = [], []
+        for release in releases:
+            for request, updates in releases[release].items():
+                for update in updates:
+                    if update.type is UpdateType.security:
+                        important.append((release, request, updates))
+                        break
+                else:
+                    normal.append((release, request, updates))
+        return important, normal
 
     def work(self, session, msg):
+        """Begin the push process.
+
+        Here we organize & prioritize the updates, and fire off seperate
+        threads for each reop tag being mashed.
+
+        If there are any security updates in the push, then those repositories
+        will be executed before all others.
+        """
         body = msg['body']['msg']
         notifications.publish(topic="mashtask.start", msg=dict())
         releases = self.organize_updates(session, body)
 
-        # Fire off seperate masher threads for each tag being mashed
-        threads = []
-        for release in releases:
-            for request, updates in releases[release].items():
-                thread = MasherThread(release, request, updates, self.log,
-                                      self.db_factory, self.mash_dir)
-                threads.append(thread)
-                thread.start()
-        for thread in threads:
-            thread.join()
+        # Important repos first, then normal
+        for batch in self.prioritize_updates(releases):
+            # Stable first, then testing
+            for req in ('stable', 'testing'):
+                threads = []
+                for release, request, updates in batch:
+                    if request == req:
+                        updates = [update.title for update in updates]
+                        log.debug('Starting thread for %s %s for %d updates',
+                                  release, request, len(updates))
+                        thread = MasherThread(release, request, updates,
+                                              self.log, self.db_factory,
+                                              self.mash_dir)
+                        threads.append(thread)
+                        thread.start()
+                for thread in threads:
+                    thread.join()
 
         self.log.info('Push complete!')
 
@@ -119,7 +153,7 @@ def organize_updates(self, session, body):
         for title in body['updates'].split():
             update = session.query(Update).filter_by(title=title).first()
             if update:
-                releases[update.release.name][update.request.value].append(update.title)
+                releases[update.release.name][update.request.value].append(update)
             else:
                 self.log.warn('Cannot find update: %s' % title)
         return releases
@@ -295,7 +329,8 @@ def expire_buildroot_overrides(self):
         for update in self.updates:
             if update.request is UpdateRequest.stable:
                 for build in update.builds:
-                    build.override.expire()
+                    if build.override:
+                        build.override.expire()
 
     def remove_pending_tags(self):
         """ Remove all pending tags from these updates """


Test patch:
diff --git a/bodhi/tests/test_masher.py b/bodhi/tests/test_masher.py
--- a/bodhi/tests/test_masher.py
+++ b/bodhi/tests/test_masher.py
@@ -28,7 +28,8 @@
 from bodhi.masher import Masher, MasherThread
 from bodhi.models import (DBSession, Base, Update, User, Group, Release,
                           Package, Build, TestCase, UpdateRequest, UpdateType,
-                          Bug, CVE, Comment, ReleaseState, BuildrootOverride)
+                          Bug, CVE, Comment, ReleaseState, BuildrootOverride,
+                          UpdateStatus)
 from bodhi.tests import populate
 
 from bodhi.util import mkmetadatadir
@@ -319,3 +320,154 @@ def test_stage(self):
         stage_dir = config.get('mash_stage_dir')
         link = os.path.join(stage_dir, t.id)
         self.assertTrue(os.path.islink(link))
+
+    @mock.patch('bodhi.masher.MasherThread.sanity_check_repo')
+    @mock.patch('bodhi.masher.MasherThread.stage_repo')
+    @mock.patch('bodhi.masher.MasherThread.generate_updateinfo')
+    @mock.patch('bodhi.notifications.publish')
+    def test_security_update_priority(self, publish, *args):
+        with self.db_factory() as db:
+            up = db.query(Update).one()
+            user = db.query(User).one()
+
+            # Create a security update for a different release
+            release = Release(
+                name=u'F18', long_name=u'Fedora 18',
+                id_prefix=u'FEDORA', version='18',
+                dist_tag=u'f18', stable_tag=u'f18-updates',
+                testing_tag=u'f18-updates-testing',
+                candidate_tag=u'f18-updates-candidate',
+                pending_testing_tag=u'f18-updates-testing-pending',
+                pending_stable_tag=u'f18-updates-pending',
+                override_tag=u'f18-override')
+            db.add(release)
+            build = Build(nvr=u'bodhi-2.0-1.fc18', release=release,
+                          package=up.builds[0].package)
+            db.add(build)
+            update = Update(
+                title=u'bodhi-2.0-1.fc18',
+                builds=[build], user=user,
+                status=UpdateStatus.testing,
+                request=UpdateRequest.stable,
+                notes=u'Useful details!', release=release)
+            update.type = UpdateType.security
+            db.add(update)
+
+            # Wipe out the tag cache so it picks up our new release
+            Release._tag_cache = None
+
+        self.msg['body']['msg']['updates'] += ' bodhi-2.0-1.fc18'
+
+        self.masher.consume(self.msg)
+
+        # Ensure that F18 runs before F17
+        calls = publish.mock_calls
+        self.assertEquals(calls[1], mock.call(msg={'repo': u'f18-updates',
+            'updates': [u'bodhi-2.0-1.fc18']}, topic='mashtask.mashing'))
+        self.assertEquals(calls[3], mock.call(msg={'success': True},
+            topic='mashtask.complete'))
+        self.assertEquals(calls[4], mock.call(msg={'repo': u'f17-updates-testing',
+            'updates': [u'bodhi-2.0-1.fc17']}, topic='mashtask.mashing'))
+        self.assertEquals(calls[-1], mock.call(msg={'success': True},
+            topic='mashtask.complete'))
+
+    @mock.patch('bodhi.masher.MasherThread.sanity_check_repo')
+    @mock.patch('bodhi.masher.MasherThread.stage_repo')
+    @mock.patch('bodhi.masher.MasherThread.generate_updateinfo')
+    @mock.patch('bodhi.notifications.publish')
+    def test_security_update_priority_testing(self, publish, *args):
+        with self.db_factory() as db:
+            up = db.query(Update).one()
+            up.type = UpdateType.security
+            up.request = UpdateRequest.testing
+            user = db.query(User).one()
+
+            # Create a security update for a different release
+            release = Release(
+                name=u'F18', long_name=u'Fedora 18',
+                id_prefix=u'FEDORA', version='18',
+                dist_tag=u'f18', stable_tag=u'f18-updates',
+                testing_tag=u'f18-updates-testing',
+                candidate_tag=u'f18-updates-candidate',
+                pending_testing_tag=u'f18-updates-testing-pending',
+                pending_stable_tag=u'f18-updates-pending',
+                override_tag=u'f18-override')
+            db.add(release)
+            build = Build(nvr=u'bodhi-2.0-1.fc18', release=release,
+                          package=up.builds[0].package)
+            db.add(build)
+            update = Update(
+                title=u'bodhi-2.0-1.fc18',
+                builds=[build], user=user,
+                status=UpdateStatus.testing,
+                request=UpdateRequest.stable,
+                notes=u'Useful details!', release=release)
+            update.type = UpdateType.enhancement
+            db.add(update)
+
+            # Wipe out the tag cache so it picks up our new release
+            Release._tag_cache = None
+
+        self.msg['body']['msg']['updates'] += ' bodhi-2.0-1.fc18'
+
+        self.masher.consume(self.msg)
+
+        # Ensure that F17 updates-testing runs before F18
+        calls = publish.mock_calls
+        self.assertEquals(calls[1], mock.call(msg={'repo': u'f17-updates-testing',
+            'updates': [u'bodhi-2.0-1.fc17']}, topic='mashtask.mashing'))
+        self.assertEquals(calls[3], mock.call(msg={'success': True},
+            topic='mashtask.complete'))
+        self.assertEquals(calls[4], mock.call(msg={'repo': u'f18-updates',
+            'updates': [u'bodhi-2.0-1.fc18']}, topic='mashtask.mashing'))
+        self.assertEquals(calls[-1], mock.call(msg={'success': True},
+            topic='mashtask.complete'))
+
+    @mock.patch('bodhi.masher.MasherThread.sanity_check_repo')
+    @mock.patch('bodhi.masher.MasherThread.stage_repo')
+    @mock.patch('bodhi.masher.MasherThread.generate_updateinfo')
+    @mock.patch('bodhi.notifications.publish')
+    def test_security_updates_parallel(self, publish, *args):
+        with self.db_factory() as db:
+            up = db.query(Update).one()
+            up.type = UpdateType.security
+            up.status = UpdateStatus.testing
+            up.request = UpdateRequest.stable
+            user = db.query(User).one()
+
+            # Create a security update for a different release
+            release = Release(
+                name=u'F18', long_name=u'Fedora 18',
+                id_prefix=u'FEDORA', version='18',
+                dist_tag=u'f18', stable_tag=u'f18-updates',
+                testing_tag=u'f18-updates-testing',
+                candidate_tag=u'f18-updates-candidate',
+                pending_testing_tag=u'f18-updates-testing-pending',
+                pending_stable_tag=u'f18-updates-pending',
+                override_tag=u'f18-override')
+            db.add(release)
+            build = Build(nvr=u'bodhi-2.0-1.fc18', release=release,
+                          package=up.builds[0].package)
+            db.add(build)
+            update = Update(
+                title=u'bodhi-2.0-1.fc18',
+                builds=[build], user=user,
+                status=UpdateStatus.testing,
+                request=UpdateRequest.stable,
+                notes=u'Useful details!', release=release)
+            update.type = UpdateType.security
+            db.add(update)
+
+            # Wipe out the tag cache so it picks up our new release
+            Release._tag_cache = None
+
+        self.msg['body']['msg']['updates'] += ' bodhi-2.0-1.fc18'
+
+        self.masher.consume(self.msg)
+
+        # Ensure that F18 and F17 run in parallel
+        calls = publish.mock_calls
+        self.assertEquals(calls[1], mock.call(msg={'repo': u'f18-updates',
+            'updates': [u'bodhi-2.0-1.fc18']}, topic='mashtask.mashing'))
+        self.assertEquals(calls[2], mock.call(msg={'repo': u'f17-updates',
+            'updates': [u'bodhi-2.0-1.fc17']}, topic='mashtask.mashing'))