From cea4d15d7d8b90afc16e8b074b88faa41294c365 Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 7 Jan 2017 12:06:52 -0600 Subject: [PATCH] bug 9855; fix db undo/redo operation to delay signal emission Certain multiple commit transactions can leave the db in an inconsistent state in between commits. If signals are emitted at each commit, GUI elements can see the inconsistent state and report errors. This fix delays the signal emission until all the commits are complete, presuming that the db is consistent before and after the complete transaction. --- gramps/gen/db/generic.py | 48 ++++++++++++++++++++++------- gramps/plugins/db/bsddb/undoredo.py | 40 +++++++++++++++++++----- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/gramps/gen/db/generic.py b/gramps/gen/db/generic.py index ca476cd9e..7100e8376 100644 --- a/gramps/gen/db/generic.py +++ b/gramps/gen/db/generic.py @@ -144,7 +144,15 @@ class DbGenericUndo(DbUndo): if key == REFERENCE_KEY: self.undo_reference(new_data, handle) else: - self.undo_data(new_data, handle, key, db.emit, SIGBASE[key]) + self.undo_data(new_data, handle, key) + # now emit the signals + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + + if key != REFERENCE_KEY: + self.undo_signals(new_data, handle, key, + db.emit, SIGBASE[key]) self.db._txn_commit() except: self.db._txn_abort() @@ -187,8 +195,15 @@ class DbGenericUndo(DbUndo): if key == REFERENCE_KEY: self.undo_reference(old_data, handle) else: - self.undo_data(old_data, handle, key, db.emit, SIGBASE[key]) + self.undo_data(old_data, handle, key) + # now emit the signals + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + if key != REFERENCE_KEY: + self.undo_signals(old_data, handle, key, + db.emit, SIGBASE[key]) self.db._txn_commit() except: self.db._txn_abort() @@ -224,7 +239,26 @@ class DbGenericUndo(DbUndo): "VALUES(?, ?, ?, ?)") self.db.dbapi.execute(sql, data) - def undo_data(self, data, handle, obj_key, emit, signal_root): + def undo_data(self, data, handle, obj_key): + """ + Helper method to undo/redo the changes made + """ + cls = KEY_TO_CLASS_MAP[obj_key] + table = cls.lower() + if data is None: + sql = "DELETE FROM %s WHERE handle = ?" % table + self.db.dbapi.execute(sql, [handle]) + else: + if self.db.has_handle(obj_key, handle): + sql = "UPDATE %s SET blob_data = ? WHERE handle = ?" % table + self.db.dbapi.execute(sql, [pickle.dumps(data), handle]) + else: + sql = "INSERT INTO %s (handle, blob_data) VALUES (?, ?)" % table + self.db.dbapi.execute(sql, [handle, pickle.dumps(data)]) + obj = self.db.get_table_func(cls)["class_func"].create(data) + self.db._update_secondary_values(obj) + + def undo_signals(self, data, handle, obj_key, emit, signal_root): """ Helper method to undo/redo the changes made """ @@ -232,19 +266,11 @@ class DbGenericUndo(DbUndo): table = cls.lower() if data is None: emit(signal_root + '-delete', ([handle],)) - sql = "DELETE FROM %s WHERE handle = ?" % table - self.db.dbapi.execute(sql, [handle]) else: if self.db.has_handle(obj_key, handle): signal = signal_root + '-update' - sql = "UPDATE %s SET blob_data = ? WHERE handle = ?" % table - self.db.dbapi.execute(sql, [pickle.dumps(data), handle]) else: signal = signal_root + '-add' - sql = "INSERT INTO %s (handle, blob_data) VALUES (?, ?)" % table - self.db.dbapi.execute(sql, [handle, pickle.dumps(data)]) - obj = self.db.get_table_func(cls)["class_func"].create(data) - self.db._update_secondary_values(obj) emit(signal, ([handle],)) class Cursor: diff --git a/gramps/plugins/db/bsddb/undoredo.py b/gramps/plugins/db/bsddb/undoredo.py index be62072ce..21e1951e3 100644 --- a/gramps/plugins/db/bsddb/undoredo.py +++ b/gramps/plugins/db/bsddb/undoredo.py @@ -237,8 +237,15 @@ class DbUndo: if key == REFERENCE_KEY: self.undo_reference(old_data, handle, self.mapbase[key]) else: - self.undo_data(old_data, handle, self.mapbase[key], - db.emit, _SIGBASE[key]) + self.undo_data(old_data, handle, self.mapbase[key]) + # now emit the signals + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + + if key != REFERENCE_KEY: + self.undo_signals(old_data, handle, self.mapbase[key], + db.emit, _SIGBASE[key]) # Notify listeners if db.undo_callback: if self.undo_count > 0: @@ -275,8 +282,15 @@ class DbUndo: if key == REFERENCE_KEY: self.undo_reference(new_data, handle, self.mapbase[key]) else: - self.undo_data(new_data, handle, self.mapbase[key], - db.emit, _SIGBASE[key]) + self.undo_data(new_data, handle, self.mapbase[key]) + # Process all signals in the transaction + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + + if key != REFERENCE_KEY: + self.undo_signals(new_data, handle, self.mapbase[key], + db.emit, _SIGBASE[key]) # Notify listeners if db.undo_callback: db.undo_callback(_("_Undo %s") @@ -308,21 +322,33 @@ class DbUndo: self.db._log_error() raise DbError(msg) - def undo_data(self, data, handle, db_map, emit, signal_root): + def undo_data(self, data, handle, db_map): + """ + Helper method to undo/redo the changes made + """ + try: + if data is None: + db_map.delete(handle, txn=self.txn) + else: + db_map.put(handle, data, txn=self.txn) + + except DBERRS as msg: + self.db._log_error() + raise DbError(msg) + + def undo_signals(self, data, handle, db_map, emit, signal_root): """ Helper method to undo/redo the changes made """ try: if data is None: emit(signal_root + '-delete', ([handle.decode('utf-8')],)) - db_map.delete(handle, txn=self.txn) else: ex_data = db_map.get(handle, txn=self.txn) if ex_data: signal = signal_root + '-update' else: signal = signal_root + '-add' - db_map.put(handle, data, txn=self.txn) emit(signal, ([handle.decode('utf-8')],)) except DBERRS as msg: