Repository: fedora-infra/bodhi

Issue:
The TransactionalSessionMaker will eat error messages if session.rollback() fails
It is possible for the call to [```session.rollback()```](https://github.com/fedora-infra/bodhi/blob/2.6.0/bodhi/server/util.py#L659) in ```bodhi.server.util.TransactionalSessionMaker``` to raise an ```Exception```. When this happens, the call to ```raise``` is not hit and we lose the information about the original ```Exception``` which makes debugging difficult.

We should probably wrap the call to ```rollback``` in another ```try/except``` block with its own log statement so we can know about both the ```rollback``` issue and the original issue.


Discussion:

Resolving patch:
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -655,9 +655,15 @@ def __call__(self):
         try:
             yield session
             session.commit()
-        except:
-            session.rollback()
-            raise
+        except Exception as e:
+            # It is possible for session.rolback() to raise Exceptions, so we will wrap it in an
+            # Exception handler as well so we can log the rollback failure and still raise the
+            # original Exception.
+            try:
+                session.rollback()
+            except:
+                log.exception('An Exception was raised while rolling back a transaction.')
+            raise e
         finally:
             session.close()
             Session.remove()


Test patch:
diff --git a/bodhi/tests/server/test_utils.py b/bodhi/tests/server/test_utils.py
--- a/bodhi/tests/server/test_utils.py
+++ b/bodhi/tests/server/test_utils.py
@@ -18,7 +18,7 @@
 from bodhi.server.buildsys import setup_buildsystem, teardown_buildsystem
 from bodhi.server.config import config
 from bodhi.server.util import (get_critpath_pkgs, get_nvr, markup,
-                               get_rpm_header, cmd, sorted_builds)
+                               get_rpm_header, cmd, sorted_builds, TransactionalSessionMaker)
 
 
 class TestUtils(object):
@@ -129,3 +129,76 @@ def test_err_zero_return_code(self, mock_popen, mock_error, mock_debug):
                                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         mock_error.assert_not_called()
         mock_debug.assert_called_with('error')
+
+
+class TestTransactionalSessionMaker(unittest.TestCase):
+    """This class contains tests on the TransactionalSessionMaker class."""
+    @mock.patch('bodhi.server.util.log.exception')
+    @mock.patch('bodhi.server.util.Session')
+    def test___call___fail_rollback_failure(self, Session, log_exception):
+        """
+        Ensure that __call__() correctly handles the failure case when rolling back itself fails.
+
+        If the wrapped code raises an Exception *and* session.rollback() itself raises an Exception,
+        __call__() should log the failure to roll back, and then close and remove the Session, and
+        should raise the original Exception again.
+        """
+        tsm = TransactionalSessionMaker()
+        exception = ValueError("u can't do that lol")
+        # Now let's make it super bad by having rollback raise an Exception
+        Session.return_value.rollback.side_effect = IOError("lol now u can't connect to the db")
+
+        with self.assertRaises(ValueError) as exc_context:
+            with tsm():
+                raise exception
+
+        log_exception.assert_called_once_with(
+            'An Exception was raised while rolling back a transaction.')
+        self.assertTrue(exc_context.exception is exception)
+        self.assertEqual(Session.return_value.commit.call_count, 0)
+        Session.return_value.rollback.assert_called_once_with()
+        Session.return_value.close.assert_called_once_with()
+        Session.remove.assert_called_once_with()
+
+    @mock.patch('bodhi.server.util.log.exception')
+    @mock.patch('bodhi.server.util.Session')
+    def test___call___fail_rollback_success(self, Session, log_exception):
+        """
+        Ensure that __call__() correctly handles the failure case when rolling back is successful.
+
+        If the wrapped code raises an Exception, __call__() should roll back the transaction, and
+        close and remove the Session, and should raise the original Exception again.
+        """
+        tsm = TransactionalSessionMaker()
+        exception = ValueError("u can't do that lol")
+
+        with self.assertRaises(ValueError) as exc_context:
+            with tsm():
+                raise exception
+
+        self.assertEqual(log_exception.call_count, 0)
+        self.assertTrue(exc_context.exception is exception)
+        self.assertEqual(Session.return_value.commit.call_count, 0)
+        Session.return_value.rollback.assert_called_once_with()
+        Session.return_value.close.assert_called_once_with()
+        Session.remove.assert_called_once_with()
+
+    @mock.patch('bodhi.server.util.log.exception')
+    @mock.patch('bodhi.server.util.Session')
+    def test___call___success(self, Session, log_exception):
+        """
+        Ensure that __call__() correctly handles the success case.
+
+        __call__() should commit the transaction, and close and remove the Session upon a successful
+        operation.
+        """
+        tsm = TransactionalSessionMaker()
+
+        with tsm():
+            pass
+
+        self.assertEqual(log_exception.call_count, 0)
+        self.assertEqual(Session.return_value.rollback.call_count, 0)
+        Session.return_value.commit.assert_called_once_with()
+        Session.return_value.close.assert_called_once_with()
+        Session.remove.assert_called_once_with()