This update is a tidy up of the code that fixed bug #927473

Changes are made due to the following issues:

- the code assumed that exception OperationalError would only be thrown by  mySQL temporarily disappearing. However, other dbms can throw this exception and usually for errors that mean a retry will also fail.

- the code repeated the actual code of the method within the exception handler. This means code duplication and also that any new exceptions  are not handled by the same exception handler so for example their transaction will not get rolled back.

- not all potential dbms exceptions were caught and so in some cases the database transaction was not rolled back

The solution is to retry transactions where an OperationalError is thrown. Currently these are retried up to 3 times before the error is logged and the method returns false. By retrying the transaction we ensure that the same transaction code with the same exception handlers is used each time.

An additional catchall exception has been added where there is a transaction to ensure that it is rolled back. As with the OperationError this throws the exception back up the stack.
This commit is contained in:
Dave Warnock 2012-06-10 22:45:02 +01:00
parent f1afae1813
commit 913b0433f6

View File

@ -238,27 +238,30 @@ class Manager(object):
``commit`` ``commit``
Commit the session with this object Commit the session with this object
""" """
try: for tryCount in range(3):
self.session.add(object_instance) try:
if commit: self.session.add(object_instance)
self.session.commit() if commit:
self.is_dirty = True self.session.commit()
return True self.is_dirty = True
except OperationalError: return True
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue - "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
self.session.add(object_instance) # way. So we only retry 3 times.
if commit: log.exception(u'Probably a MySQL issue - "MySQL has gone away"')
self.session.commit() self.session.rollback()
self.is_dirty = True if tryCount >= 2:
return True raise
except InvalidRequestError: except InvalidRequestError:
self.session.rollback() self.session.rollback()
log.exception(u'Object save failed') log.exception(u'Object list save failed')
return False return False
except:
self.session.rollback()
raise
def save_objects(self, object_list, commit=True): def save_objects(self, object_list, commit=True):
""" """
@ -270,27 +273,30 @@ class Manager(object):
``commit`` ``commit``
Commit the session with this object Commit the session with this object
""" """
try: for tryCount in range(3):
self.session.add_all(object_list) try:
if commit: self.session.add_all(object_list)
self.session.commit() if commit:
self.is_dirty = True self.session.commit()
return True self.is_dirty = True
except OperationalError: return True
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
self.session.add_all(object_list) # way. So we only retry 3 times.
if commit: log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
self.session.commit() self.session.rollback()
self.is_dirty = True if tryCount >= 2:
return True raise
except InvalidRequestError: except InvalidRequestError:
self.session.rollback() self.session.rollback()
log.exception(u'Object list save failed') log.exception(u'Object list save failed')
return False return False
except:
self.session.rollback()
raise
def get_object(self, object_class, key=None): def get_object(self, object_class, key=None):
""" """
@ -305,15 +311,18 @@ class Manager(object):
if not key: if not key:
return object_class() return object_class()
else: else:
try: for tryCount in range(3):
return self.session.query(object_class).get(key) try:
except OperationalError: return self.session.query(object_class).get(key)
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
return self.session.query(object_class).get(key) # way. So we only retry 3 times.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
if tryCount >= 2:
raise
def get_object_filtered(self, object_class, filter_clause): def get_object_filtered(self, object_class, filter_clause):
""" """
@ -325,15 +334,18 @@ class Manager(object):
``filter_clause`` ``filter_clause``
The criteria to select the object by The criteria to select the object by
""" """
try: for tryCount in range(3):
return self.session.query(object_class).filter(filter_clause).first() try:
except OperationalError: return self.session.query(object_class).filter(filter_clause).first()
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
return self.session.query(object_class).filter(filter_clause).first() # way. So we only retry 3 times.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
if tryCount >= 2:
raise
def get_all_objects(self, object_class, filter_clause=None, def get_all_objects(self, object_class, filter_clause=None,
order_by_ref=None): order_by_ref=None):
@ -357,15 +369,18 @@ class Manager(object):
query = query.order_by(*order_by_ref) query = query.order_by(*order_by_ref)
elif order_by_ref is not None: elif order_by_ref is not None:
query = query.order_by(order_by_ref) query = query.order_by(order_by_ref)
try: for tryCount in range(3):
return query.all() try:
except OperationalError: return query.all()
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
return query.all() # way. So we only retry 3 times.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
if tryCount >= 2:
raise
def get_object_count(self, object_class, filter_clause=None): def get_object_count(self, object_class, filter_clause=None):
""" """
@ -381,15 +396,18 @@ class Manager(object):
query = self.session.query(object_class) query = self.session.query(object_class)
if filter_clause is not None: if filter_clause is not None:
query = query.filter(filter_clause) query = query.filter(filter_clause)
try: for tryCount in range(3):
return query.count() try:
except OperationalError: return query.count()
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
return query.count() # way. So we only retry 3 times.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
if tryCount >= 2:
raise
def delete_object(self, object_class, key): def delete_object(self, object_class, key):
""" """
@ -403,25 +421,29 @@ class Manager(object):
""" """
if key != 0: if key != 0:
object_instance = self.get_object(object_class, key) object_instance = self.get_object(object_class, key)
try: for tryCount in range(3):
self.session.delete(object_instance) try:
self.session.commit() self.session.delete(object_instance)
self.is_dirty = True self.session.commit()
return True self.is_dirty = True
except OperationalError: return True
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
self.session.delete(object_instance) # way. So we only retry 3 times.
self.session.commit() log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
self.is_dirty = True self.session.rollback()
return True if tryCount >= 2:
except InvalidRequestError: raise
self.session.rollback() except InvalidRequestError:
log.exception(u'Failed to delete object') self.session.rollback()
return False log.exception(u'Failed to delete object')
return False
except:
self.session.rollback()
raise
else: else:
return True return True
@ -439,31 +461,32 @@ class Manager(object):
The filter governing selection of objects to return. Defaults to The filter governing selection of objects to return. Defaults to
None. None.
""" """
try: for tryCount in range(3):
query = self.session.query(object_class) try:
if filter_clause is not None: query = self.session.query(object_class)
query = query.filter(filter_clause) if filter_clause is not None:
query.delete(synchronize_session=False) query = query.filter(filter_clause)
self.session.commit() query.delete(synchronize_session=False)
self.is_dirty = True self.session.commit()
return True self.is_dirty = True
except OperationalError: return True
# This exception clause is for users running MySQL which likes except OperationalError:
# to terminate connections on its own without telling anyone. # This exception clause is for users running MySQL which likes
# See bug #927473 # to terminate connections on its own without telling anyone.
log.exception(u'Probably a MySQL issue, "MySQL has gone away"') # See bug #927473
self.session.rollback() # However, other dbms can raise it, usually in a non-recoverable
query = self.session.query(object_class) # way. So we only retry 3 times.
if filter_clause is not None: log.exception(u'Probably a MySQL issue, "MySQL has gone away"')
query = query.filter(filter_clause) self.session.rollback()
query.delete(synchronize_session=False) if tryCount >= 2:
self.session.commit() raise
self.is_dirty = True except InvalidRequestError:
return True self.session.rollback()
except InvalidRequestError: log.exception(u'Failed to delete %s records', object_class.__name__)
self.session.rollback() return False
log.exception(u'Failed to delete %s records', object_class.__name__) except:
return False self.session.rollback()
raise
def finalise(self): def finalise(self):
""" """