Repository: fedora-infra/bodhi

Issue:
null value in column "release_id" violates not-null constraint when creating updates
While testing the 3.2.0 beta today I noticed that creating updates causes a traceback:

```
2018-01-17 15:57:07,275 INFO  [bodhi.server][waitress] Creating new update: [<RpmBuild {'release_id': 17, 'nvr': u'erlang-fs-4.1-2.fc27', 'signed': False, 'epoch': 0, 'ci_url': N[50/1955]
e': u'rpm'}>]
2018-01-17 15:57:07,399 DEBUG [bodhi.server][waitress] Creating new Update(**data) object.
2018-01-17 15:57:07,407 ERROR [bodhi.server][waitress] Failed to create update
Traceback (most recent call last):
  File "/home/vagrant/bodhi/bodhi/server/services/updates.py", line 503, in new_update
    result, _caveats = Update.new(request, _data)
  File "/home/vagrant/bodhi/bodhi/server/models.py", line 1776, in new
    up.release = release
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/attributes.py", line 224, in __set__
    instance_dict(instance), value, None)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/attributes.py", line 812, in set
    value = self.fire_replace_event(state, dict_, value, old, initiator)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/attributes.py", line 834, in fire_replace_event
    self._replace_token or self._init_append_or_replace_token())
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/util.py", line 118, in set_
    return validator(state.obj(), key, value)
  File "/home/vagrant/bodhi/bodhi/server/models.py", line 1582, in validate_release
    u = Update.query.filter(Update.release_id == release.id, Update.id != self.id).first()
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/query.py", line 2755, in first
    ret = list(self[0:1])
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/query.py", line 2547, in __getitem__
    return list(res)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/query.py", line 2854, in __iter__
    self.session._autoflush()
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/session.py", line 1407, in _autoflush
    util.raise_from_cause(e)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/session.py", line 1397, in _autoflush
    self.flush()
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/session.py", line 2171, in flush
    self._flush(objects)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/session.py", line 2291, in _flush
    transaction.rollback(_capture_exception=True)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/session.py", line 2255, in _flush
    flush_context.execute()
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 389, in execute
    rec.execute(self)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 548, in execute
    uow
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 181, in save_obj
    mapper, table, insert)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 835, in _emit_insert_statements
    execute(statement, params)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/base.py", line 945, in execute
    return meth(self, multiparams, params)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/sql/elements.py", line 263, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/base.py", line 1053, in _execute_clauseelement
    compiled_sql, distilled_params
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/base.py", line 1189, in _execute_context
    context)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/base.py", line 1402, in _handle_dbapi_exception
    exc_info
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
    context)
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute
    cursor.execute(statement, parameters)
IntegrityError: (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely) (psycopg2.IntegrityError) null value in
 column "release_id" violates not-null constraint
DETAIL:  Failing row contains (104272, erlang-fs-4.1-2.fc27, 3, -3, , t, t, test, enhancement, pending, null, unspecified, unspecified, f, f, f, t, 2018-01-17 15:57:07.405564, null, null,
 null, null, null, null, 2897, null, null, t, null, null).
[SQL: 'INSERT INTO updates (title, autokarma, stable_karma, unstable_karma, requirements, require_bugs, require_testcases, notes, type, status, request, severity, suggest, locked, pushed
, critpath, close_bugs, date_submitted, date_modified, date_approved, date_pushed, date_testing, date_stable, alias, old_updateid, release_id, user_id, test_gating_status, greenwave_summary_string) VALUES (%(title)s, %(autokarma)s, %(stab
le_karma)s, %(unstable_karma)s, %(requirements)s, %(require_bugs)s, %(require_testcases)s, %(notes)s, %(type)s, %(status)s, %(request)s, %(severity)s, %(suggest)s, %(locked)s, %(pushed)s, %(critpath)s, %(close_bugs)s, %(date_submitted)s,
%(date_modified)s, %(date_approved)s, %(date_pushed)s, %(date_testing)s, %(date_stable)s, %(alias)s, %(old_updateid)s, %(release_id)s, %(user_id)s, %(test_gating_status)s, %(greenwave_summary_string)s) RETURNING updates.id'] [parameters:
{'date_testing': None, 'old_updateid': None, 'pushed': False, 'require_testcases': True, 'date_stable': None, 'critpath': False, 'date_approved': None, 'stable_karma': 3, 'date_pushed': None, 'requirements': '', 'severity': 'unspecified',
 'autokarma': True, 'title': u'erlang-fs-4.1-2.fc27', 'suggest': 'unspecified', 'require_bugs': True, 'test_gating_status': None, 'type': 'enhancement', 'close_bugs': True, 'status': 'pending', 'release_id': None, 'date_submitted': dateti
me.datetime(2018, 1, 17, 15, 57, 7, 405564), 'unstable_karma': -3, 'user_id': 2897, 'locked': False, 'date_modified': None, 'notes': u'test', 'request': None, 'alias': None, 'greenwave_summary_string': None}]
```


Discussion:

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2011-2017 Red Hat, Inc. and others.
+# Copyright © 2011-2018 Red Hat, Inc. and others.
 #
 # This file is part of Bodhi.
 #
@@ -1825,7 +1825,10 @@ def new(cls, request, data):
         log.debug("Creating new Update(**data) object.")
         release = data.pop('release', None)
         up = Update(**data)
-        up.release = release
+        # Autoflush will cause a problem for Update.validate_release().
+        # https://github.com/fedora-infra/bodhi/issues/2117
+        with util.no_autoflush(db):
+            up.release = release
 
         # Assign the alias before setting the request.
         # Setting the request publishes a fedmsg message, and it is nice to
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2007-2017 Red Hat, Inc. and others.
+# Copyright © 2007-2018 Red Hat, Inc. and others.
 #
 # This file is part of Bodhi.
 #
@@ -1351,3 +1351,29 @@ def waiverdb_api_post(waiverdb_api_url, data):
                         'Content-Type': 'application/json',
                         'Authorization': 'Bearer %s' % config.get('waiverdb.access_token')
                     })
+
+
+class no_autoflush(object):
+    """
+    A content manager that disables sqlalchemy's autoflush, restoring it afterwards.
+
+    Adapted from https://bitbucket.org/zzzeek/sqlalchemy/wiki/UsageRecipes/DisableAutoflush
+    """
+
+    def __init__(self, session):
+        """
+        Store the session and remember its entrant state.
+
+        Args:
+            session (sqlalchemy.orm.session.Session): The session to disable autoflush on.
+        """
+        self.session = session
+        self.autoflush = session.autoflush
+
+    def __enter__(self):
+        """Disable autoflush."""
+        self.session.autoflush = False
+
+    def __exit__(self, *args, **kwargs):
+        """Restore autoflush to its entrant state. Args unused."""
+        self.session.autoflush = self.autoflush


Test patch:
diff --git a/bodhi/tests/server/test_util.py b/bodhi/tests/server/test_util.py
--- a/bodhi/tests/server/test_util.py
+++ b/bodhi/tests/server/test_util.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2007-2017 Red Hat, Inc. and others.
+# Copyright © 2007-2018 Red Hat, Inc. and others.
 #
 # This file is part of Bodhi.
 #
@@ -152,6 +152,31 @@ def json(self):
         sleep.assert_called_once_with(1)
 
 
+class TestNoAutoflush(unittest.TestCase):
+    """Test the no_autoflush context manager."""
+    def test_autoflush_disabled(self):
+        """Test correct behavior when autoflush is disabled."""
+        session = mock.MagicMock()
+        session.autoflush = False
+
+        with util.no_autoflush(session):
+            self.assertFalse(session.autoflush)
+
+        # autoflush should still be False since that was the starting condition.
+        self.assertFalse(session.autoflush)
+
+    def test_autoflush_enabled(self):
+        """Test correct behavior when autoflush is enabled."""
+        session = mock.MagicMock()
+        session.autoflush = True
+
+        with util.no_autoflush(session):
+            self.assertFalse(session.autoflush)
+
+        # autoflush should again be True since that was the starting condition.
+        self.assertTrue(session.autoflush)
+
+
 class TestPushToBatchedOrStableButton(base.BaseTestCase):
     """Test the push_to_batched_or_stable_button() function."""
     def test_request_is_batched(self):