Repository: fedora-infra/bodhi

Issue:
Bodhi must be restarted after using bodhi-manage-releases to create a new release
I noticed today that I needed to restart Bodhi after using ```bodhi-manage-releases``` to create a new Release object before I could create an update that used tags from that Release. I think this is due to some [permanent caching](https://github.com/fedora-infra/bodhi/blob/3.4.0/bodhi/server/models.py#L850-L895) in the Release model code. We should probably have changes to releases reset those caches.


Discussion:
Since ```bodhi-manage-releases``` is now deprecated, this should be done in ```bodhi releases edit/create``` instead.
Note that this one will be difficult to do because Bodhi uses multiple server processes, and they must all be notified to clear their caches when releases are edited or created. There isn't currently an infrastructure in place to do this.
We will need to either start using a caching server for Bodhi's processes, or will need to make the queries efficient so we don't need a cache at all.
I believe that @nphilipp might be interested in solving this problem.

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -899,6 +899,11 @@ def all_releases(cls):
         return cls._all_releases
     _all_releases = None
 
+    @classmethod
+    def clear_all_releases_cache(cls):
+        """Clear up Release cache."""
+        cls._all_releases = None
+
     @classmethod
     def get_tags(cls, session):
         """
diff --git a/bodhi/server/services/releases.py b/bodhi/server/services/releases.py
--- a/bodhi/server/services/releases.py
+++ b/bodhi/server/services/releases.py
@@ -357,6 +357,9 @@ def save_release(request):
                         )
                 setattr(r, k, v)
 
+        # We have to invalidate the release cache after change
+        Release.clear_all_releases_cache()
+
     except Exception as e:
         log.exception(e)
         request.errors.add('body', 'release',


Test patch:
diff --git a/bodhi/tests/server/base.py b/bodhi/tests/server/base.py
--- a/bodhi/tests/server/base.py
+++ b/bodhi/tests/server/base.py
@@ -144,7 +144,7 @@ class BaseTestCaseMixin:
     def _setup_method(self):
         """Set up Bodhi for testing."""
         # Ensure "cached" objects are cleared before each test.
-        models.Release._all_releases = None
+        models.Release.clear_all_releases_cache()
         models.Release._tag_cache = None
 
         if engine is None:
@@ -293,7 +293,7 @@ def create_release(self, version, create_automatic_updates=False):
             package_manager=models.PackageManager.unspecified,
             testing_repository=None)
         self.db.add(release)
-        models.Release._all_releases = None
+        models.Release.clear_all_releases_cache()
         models.Release._tag_cache = None
         self.db.flush()
         return release
diff --git a/bodhi/tests/server/consumers/test_composer.py b/bodhi/tests/server/consumers/test_composer.py
--- a/bodhi/tests/server/consumers/test_composer.py
+++ b/bodhi/tests/server/consumers/test_composer.py
@@ -161,7 +161,7 @@ def setUp(self):
         self.handler = ComposerHandler(db_factory=self.db_factory, compose_dir=self.tempdir)
 
         # Reset "cached" objects before each test.
-        Release._all_releases = None
+        Release.clear_all_releases_cache()
         Release._tag_cache = None
 
         self.expected_sems = 0
diff --git a/bodhi/tests/server/services/test_releases.py b/bodhi/tests/server/services/test_releases.py
--- a/bodhi/tests/server/services/test_releases.py
+++ b/bodhi/tests/server/services/test_releases.py
@@ -201,6 +201,16 @@ def test_new_release(self):
 
         self.assertEqual(r.state, ReleaseState.disabled)
 
+        # Let's check Release cache content
+        releases = Release.all_releases()
+        disabled_releases = releases["disabled"]
+        self.assertEqual(disabled_releases[0]["name"], "F42")
+        self.assertEqual(disabled_releases[1]["name"], "F22")
+        self.assertEqual(len(disabled_releases), 2)
+        current_releases = releases["current"]
+        self.assertEqual(current_releases[0]["name"], "F17")
+        self.assertEqual(len(current_releases), 1)
+
     def test_list_releases_by_current_state(self):
         """ Test that we can filter releases using the 'current' state """
         res = self.app.get('/releases/', {"state": 'current'})
diff --git a/bodhi/tests/server/test___init__.py b/bodhi/tests/server/test___init__.py
--- a/bodhi/tests/server/test___init__.py
+++ b/bodhi/tests/server/test___init__.py
@@ -271,8 +271,8 @@ def test_sets_up_home_page_cache(self, _generate_home_page_stats):
     @mock.patch.dict('bodhi.server.config.config', {'warm_cache_on_start': True})
     def test_warms_up_releases_cache(self):
         """main() should warm up the _all_releases cache."""
-        # Let's force the release cache to None
-        models.Release._all_releases = None
+        # Let's clear the release cache
+        models.Release.clear_all_releases_cache()
 
         server.main({}, testing='guest', session=self.db)
 
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -745,6 +745,12 @@ def test_all_releases(self):
         # Make sure it's the same cached object
         self.assertIs(releases, model.Release.all_releases())
 
+    def test_clear_all_releases_cache(self):
+        model.Release.all_releases()
+        self.assertIsNotNone(model.Release._all_releases)
+        model.Release.clear_all_releases_cache()
+        self.assertIsNone(model.Release._all_releases)
+
 
 class TestReleaseCritpathMinKarma(BaseTestCase):
     """Tests for the Release.critpath_min_karma property."""
@@ -807,6 +813,12 @@ def test_all_releases(self):
         # Make sure it's the same cached object
         self.assertIs(releases, model.Release.all_releases())
 
+    def test_clear_all_releases_cache(self):
+        model.Release.all_releases()
+        self.assertIsNotNone(model.Release._all_releases)
+        model.Release.clear_all_releases_cache()
+        self.assertIsNone(model.Release._all_releases)
+
 
 class TestReleaseContainer(ModelTest):
     """Unit test case for the ``Release`` model for container releases."""
@@ -841,6 +853,12 @@ def test_all_releases(self):
         # Make sure it's the same cached object
         self.assertIs(releases, model.Release.all_releases())
 
+    def test_clear_all_releases_cache(self):
+        model.Release.all_releases()
+        self.assertIsNotNone(model.Release._all_releases)
+        model.Release.clear_all_releases_cache()
+        self.assertIsNone(model.Release._all_releases)
+
 
 class TestReleaseFlatpak(ModelTest):
     """Unit test case for the ``Release`` model for flatpak releases."""
@@ -875,6 +893,12 @@ def test_all_releases(self):
         # Make sure it's the same cached object
         self.assertIs(releases, model.Release.all_releases())
 
+    def test_clear_all_releases_cache(self):
+        model.Release.all_releases()
+        self.assertIsNotNone(model.Release._all_releases)
+        model.Release.clear_all_releases_cache()
+        self.assertIsNone(model.Release._all_releases)
+
 
 class MockWiki(object):
     """ Mocked simplemediawiki.MediaWiki class. """