GP-2183 corrected potential deadlock with various DBListener callbacks

This commit is contained in:
ghidra1 2022-06-16 17:11:22 -04:00
parent 0e3fe30c67
commit 222850142a
7 changed files with 323 additions and 90 deletions

View file

@ -47,6 +47,7 @@ public class DBHandle {
private long lastTransactionID; private long lastTransactionID;
private boolean txStarted = false; private boolean txStarted = false;
private boolean waitingForNewTransaction = false; private boolean waitingForNewTransaction = false;
private boolean reloadInProgress = false;
private long checkpointNum; private long checkpointNum;
private long lastRecoverySnapshotId; private long lastRecoverySnapshotId;
@ -316,8 +317,10 @@ public class DBHandle {
* Close the scratch-pad database handle if it open. * Close the scratch-pad database handle if it open.
*/ */
public void closeScratchPad() { public void closeScratchPad() {
if (scratchPad != null) { // use copy of scratchPad to be thread-safe
scratchPad.close(); DBHandle scratchDbh = scratchPad;
if (scratchDbh != null) {
scratchDbh.close();
scratchPad = null; scratchPad = null;
} }
} }
@ -330,25 +333,31 @@ public class DBHandle {
listenerList.add(listener); listenerList.add(listener);
} }
private void dbRestored() { private void notifyDbRestored() {
for (DBListener listener : listenerList) { for (DBListener listener : listenerList) {
listener.dbRestored(this); listener.dbRestored(this);
} }
} }
private void dbClosed() { private void notifyDbClosed() {
for (DBListener listener : listenerList) { for (DBListener listener : listenerList) {
listener.dbClosed(this); listener.dbClosed(this);
} }
} }
private void tableAdded(Table table) { private void notifyTableAdded(Table table) {
if (reloadInProgress) {
return; // squash notification during reload (e.g., undo/redo)
}
for (DBListener listener : listenerList) { for (DBListener listener : listenerList) {
listener.tableAdded(this, table); listener.tableAdded(this, table);
} }
} }
void tableDeleted(Table table) { void notifyTableDeleted(Table table) {
if (reloadInProgress) {
return; // squash notification during reload (e.g., undo/redo)
}
for (DBListener listener : listenerList) { for (DBListener listener : listenerList) {
listener.tableDeleted(this, table); listener.tableDeleted(this, table);
} }
@ -418,15 +427,39 @@ public class DBHandle {
} }
/** /**
* Terminate transaction. If commit is false, Table instances may be added * End current transaction. If commit is false a rollback may occur followed by
* or removed/invalidated. * {@link DBListener#dbRestored(DBHandle)} notification to listeners.
*
* @param id transaction ID * @param id transaction ID
* @param commit if true a new checkpoint will be established, if * @param commit if true a new checkpoint will be established for active transaction, if
* false all changes since the previous checkpoint will be discarded. * false all changes since the previous checkpoint will be discarded.
* @return true if new checkpoint established. * @return true if new checkpoint established, false if nothing to commit
* or commit parameter specified as false and active transaction is terminated with rollback.
* @throws IOException if IO error occurs * @throws IOException if IO error occurs
*/ */
public synchronized boolean endTransaction(long id, boolean commit) throws IOException { public boolean endTransaction(long id, boolean commit) throws IOException {
try {
return doEndTransaction(id, commit);
}
catch (DBRollbackException e) {
notifyDbRestored();
}
return false;
}
/**
* End current transaction. If <code>commit</code> is false a rollback may be perfromed.
*
* @param id transaction ID
* @param commit if true a new checkpoint will be established for active transaction, if
* false all changes since the previous checkpoint will be discarded.
* @return true if new checkpoint established.
* @throws DBRollbackException if <code>commit</code> is false and active transaction was
* terminated and database rollback was performed (i.e., undo performed).
* @throws IOException if IO error occurs
*/
private synchronized boolean doEndTransaction(long id, boolean commit)
throws DBRollbackException, IOException {
if (id != lastTransactionID) { if (id != lastTransactionID) {
throw new IllegalStateException("Transaction id is not active"); throw new IllegalStateException("Transaction id is not active");
} }
@ -440,9 +473,11 @@ public class DBHandle {
} }
return false; return false;
} }
// rollback
// rollback transaction
bufferMgr.undo(false); bufferMgr.undo(false);
reloadTables(); reloadTables();
throw new DBRollbackException();
} }
} }
finally { finally {
@ -459,9 +494,33 @@ public class DBHandle {
return (bufferMgr != null && !bufferMgr.atCheckpoint()); return (bufferMgr != null && !bufferMgr.atCheckpoint());
} }
public synchronized void terminateTransaction(long id, boolean commit) throws IOException { /**
endTransaction(id, commit); * Terminate current transaction. If commit is false a rollback may occur followed by
waitingForNewTransaction = true; * {@link DBListener#dbRestored(DBHandle)} notification to listeners. This method is very
* similar to {@link #endTransaction(long, boolean)} with the added behavior of setting the
* internal {@link DBHandle} state such that any subsequent invocations of
* {@link #checkTransaction()} will throw a {@link TerminatedTransactionException} until a new
* transaction is started.
*
* @param id transaction ID
* @param commit if true a new checkpoint will be established for active transaction, if
* false all changes since the previous checkpoint will be discarded.
* @throws IOException if IO error occurs
*/
public void terminateTransaction(long id, boolean commit) throws IOException {
boolean rollback = false;
synchronized (this) {
try {
doEndTransaction(id, commit);
}
catch (DBRollbackException e) {
rollback = true;
}
waitingForNewTransaction = true;
}
if (rollback) {
notifyDbRestored();
}
} }
/** /**
@ -476,16 +535,22 @@ public class DBHandle {
* Undo changes made during the previous transaction checkpoint. * Undo changes made during the previous transaction checkpoint.
* All upper-levels must clear table-based cached data prior to * All upper-levels must clear table-based cached data prior to
* invoking this method. * invoking this method.
* @return true if an undo was successful * @return true if an undo was successful, else false if not allowed
* @throws IOException if IO error occurs * @throws IOException if IO error occurs
*/ */
public synchronized boolean undo() throws IOException { public boolean undo() throws IOException {
if (canUndo() && bufferMgr.undo(true)) { boolean success = false;
++checkpointNum; synchronized (this) {
reloadTables(); if (canUndo() && bufferMgr.undo(true)) {
return true; ++checkpointNum;
reloadTables();
success = true;
}
} }
return false; if (success) {
notifyDbRestored();
}
return success;
} }
/** /**
@ -515,16 +580,22 @@ public class DBHandle {
* Moves forward by one checkpoint only. * Moves forward by one checkpoint only.
* All upper-levels must clear table-based cached data prior to * All upper-levels must clear table-based cached data prior to
* invoking this method. * invoking this method.
* @return boolean * @return boolean if redo is successful, else false if undo not allowed
* @throws IOException if IO error occurs * @throws IOException if IO error occurs
*/ */
public synchronized boolean redo() throws IOException { public boolean redo() throws IOException {
if (canRedo() && bufferMgr.redo()) { boolean success = false;
++checkpointNum; synchronized (this) {
reloadTables(); if (canRedo() && bufferMgr.redo()) {
return true; ++checkpointNum;
reloadTables();
success = true;
}
} }
return false; if (success) {
notifyDbRestored();
}
return success;
} }
/** /**
@ -569,7 +640,7 @@ public class DBHandle {
* Close the database and dispose of the underlying buffer manager. * Close the database and dispose of the underlying buffer manager.
* Any existing recovery data will be discarded. * Any existing recovery data will be discarded.
*/ */
public synchronized void close() { public void close() {
close(false); close(false);
} }
@ -578,11 +649,15 @@ public class DBHandle {
* @param keepRecoveryData true if existing recovery data should be retained or false to remove * @param keepRecoveryData true if existing recovery data should be retained or false to remove
* any recovery data * any recovery data
*/ */
public synchronized void close(boolean keepRecoveryData) { public void close(boolean keepRecoveryData) {
closeScratchPad(); closeScratchPad();
if (bufferMgr != null) {
dbClosed(); // use copy of bufferMgr to be thread-safe
bufferMgr.dispose(keepRecoveryData); BufferMgr mgr = bufferMgr;
if (mgr != null) {
notifyDbClosed();
mgr.dispose(keepRecoveryData);
bufferMgr = null; bufferMgr = null;
} }
} }
@ -844,32 +919,38 @@ public class DBHandle {
*/ */
private void reloadTables() throws IOException { private void reloadTables() throws IOException {
dbParms.refresh(); reloadInProgress = true;
try {
dbParms.refresh();
Hashtable<String, Table> oldTables = tables; Hashtable<String, Table> oldTables = tables;
tables = new Hashtable<>(); tables = new Hashtable<>();
TableRecord[] tableRecords = masterTable.refreshTableRecords(); // NOTE: master table invalidates any obsolete tables during refresh
for (TableRecord tableRecord : tableRecords) { TableRecord[] tableRecords = masterTable.refreshTableRecords();
for (TableRecord tableRecord : tableRecords) {
String tableName = tableRecord.getName(); String tableName = tableRecord.getName();
// Process each primary tables // Process each primary tables
if (tableRecord.getIndexedColumn() < 0) { if (tableRecord.getIndexedColumn() < 0) {
Table t = oldTables.get(tableName); Table t = oldTables.get(tableName);
if (t == null || t.isInvalid()) { if (t == null || t.isInvalid()) {
oldTables.remove(tableName); oldTables.remove(tableName);
t = new Table(this, tableRecord); t = new Table(this, tableRecord);
tableAdded(t); notifyTableAdded(t);
}
tables.put(tableName, t);
} }
tables.put(tableName, t);
}
// secondary table indexes // secondary table indexes
else if (!oldTables.containsKey(tableName)) { else if (!oldTables.containsKey(tableName)) {
IndexTable.getIndexTable(this, tableRecord); IndexTable.getIndexTable(this, tableRecord);
}
} }
} }
dbRestored(); finally {
reloadInProgress = false;
}
} }
/** /**
@ -917,20 +998,23 @@ public class DBHandle {
* @return new table instance * @return new table instance
* @throws IOException if IO error occurs during table creation * @throws IOException if IO error occurs during table creation
*/ */
public synchronized Table createTable(String name, Schema schema, int[] indexedColumns) public Table createTable(String name, Schema schema, int[] indexedColumns)
throws IOException { throws IOException {
if (tables.containsKey(name)) { Table table;
throw new IOException("Table already exists"); synchronized (this) {
} if (tables.containsKey(name)) {
checkTransaction(); throw new IOException("Table already exists");
Table table = new Table(this, masterTable.createTableRecord(name, schema, -1)); }
tables.put(name, table); checkTransaction();
if (indexedColumns != null) { table = new Table(this, masterTable.createTableRecord(name, schema, -1));
for (int indexedColumn : indexedColumns) { tables.put(name, table);
IndexTable.createIndexTable(table, indexedColumn); if (indexedColumns != null) {
for (int indexedColumn : indexedColumns) {
IndexTable.createIndexTable(table, indexedColumn);
}
} }
} }
tableAdded(table); notifyTableAdded(table);
return table; return table;
} }
@ -964,19 +1048,23 @@ public class DBHandle {
* @param name table name * @param name table name
* @throws IOException if there is an I/O error or the table does not exist * @throws IOException if there is an I/O error or the table does not exist
*/ */
public synchronized void deleteTable(String name) throws IOException { public void deleteTable(String name) throws IOException {
Table table = tables.get(name); Table table;
if (table == null) { synchronized (this) {
return; table = tables.get(name);
if (table == null) {
return;
}
checkTransaction();
int[] indexedColumns = table.getIndexedColumns();
for (int indexedColumn : indexedColumns) {
table.removeIndex(indexedColumn);
}
table.deleteAll();
masterTable.deleteTableRecord(table.getTableNum());
tables.remove(name);
} }
checkTransaction(); notifyTableDeleted(table);
int[] indexedColumns = table.getIndexedColumns();
for (int indexedColumn : indexedColumns) {
table.removeIndex(indexedColumn);
}
table.deleteAll();
masterTable.deleteTableRecord(table.getTableNum());
tables.remove(name);
} }
/** /**
@ -1000,9 +1088,6 @@ public class DBHandle {
return bufferMgr.getLowBufferCount(); return bufferMgr.getLowBufferCount();
} }
/*
* @see java.lang.Object#finalize()
*/
@Override @Override
protected void finalize() throws Throwable { protected void finalize() throws Throwable {
close(true); close(true);

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,9 +22,9 @@ public interface DBListener {
/** /**
* Provides notification that an undo or redo was performed. * Provides notification that an undo or redo was performed.
* Separate notification will be provided if tables were added/removed. * During the restore process {@link #tableAdded(DBHandle, Table)} and
* The state of the database may still be in transition and should not be accessed * {@link #tableDeleted(DBHandle, Table)} notifications will be supressed.
* by this callback method. * Any listener concerned with tables added or removed should reacquire their table(s).
* @param dbh associated database handle * @param dbh associated database handle
*/ */
void dbRestored(DBHandle dbh); void dbRestored(DBHandle dbh);

View file

@ -0,0 +1,30 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package db;
/**
* <code>DBRollbackException</code> thrown when a database transaction rollback was performed
* during transaction termination.
*/
class DBRollbackException extends Exception {
/**
* Construct exception
*/
DBRollbackException() {
super();
}
}

View file

@ -112,14 +112,10 @@ public class Table {
* Subsequent table use may generate an exception. * Subsequent table use may generate an exception.
*/ */
void invalidate() { void invalidate() {
boolean isIndexTable = tableRecord.getIndexedColumn() >= 0;
tableRecord = null; tableRecord = null;
rootBufferId = -1; rootBufferId = -1;
nodeMgr = null; nodeMgr = null;
++modCount; ++modCount;
if (!isIndexTable) {
db.tableDeleted(this);
}
} }
/** /**

View file

@ -19,7 +19,7 @@ import static org.junit.Assert.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.*;
import org.junit.*; import org.junit.*;
@ -46,12 +46,15 @@ public class DBTest extends AbstractGenericTest {
private BufferFileManager fileMgr; private BufferFileManager fileMgr;
private DBHandle dbh; private DBHandle dbh;
private BufferFile bfile; private BufferFile bfile;
private MyDbListener listener;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
testDir = createTempDirectory(getClass().getSimpleName()); testDir = createTempDirectory(getClass().getSimpleName());
dbh = new DBHandle(BUFFER_SIZE, CACHE_SIZE); dbh = new DBHandle(BUFFER_SIZE, CACHE_SIZE);
listener = new MyDbListener();
dbh.addListener(listener);
} }
@After @After
@ -548,4 +551,108 @@ public class DBTest extends AbstractGenericTest {
assertNotNull(t1prime.getRecord(1)); assertNotNull(t1prime.getRecord(1));
} }
@Test
public void testEvents() throws IOException {
createIndexedTables(false);
// Verify table added events
Table[] tables = dbh.getTables();
assertEquals(11, tables.length);
assertEquals(tables.length, listener.events.size());
int ix = 0;
for (DbEvent evt : listener.events) {
assertEquals(DbNotifyType.TABLE_ADDED, evt.type);
assertEquals("TABLE" + ix, evt.table.getName());
++ix;
}
listener.events.clear();
// Verify table deleted event
long txId = dbh.startTransaction();
Table t = dbh.getTable("TABLE5");
dbh.deleteTable("TABLE5");
dbh.endTransaction(txId, true);
assertEquals(1, listener.events.size());
DbEvent evt = listener.events.get(0);
assertEquals(DbNotifyType.TABLE_DELETED, evt.type);
assertTrue(t == evt.table);
listener.events.clear();
dbh.undo();
assertEquals(1, listener.events.size());
evt = listener.events.get(0);
assertEquals(DbNotifyType.RESTORED, evt.type);
listener.events.clear();
dbh.redo();
assertEquals(1, listener.events.size());
evt = listener.events.get(0);
assertEquals(DbNotifyType.RESTORED, evt.type);
listener.events.clear();
dbh.close();
dbh = null;
assertEquals(1, listener.events.size());
evt = listener.events.get(0);
assertEquals(DbNotifyType.CLOSED, evt.type);
}
private enum DbNotifyType {
RESTORED, CLOSED, TABLE_DELETED, TABLE_ADDED;
}
private static class DbEvent {
final DbNotifyType type;
final Table table;
DbEvent(DbNotifyType type, Table table) {
this.type = type;
this.table = table;
}
@Override
public String toString() {
if (type != DbNotifyType.TABLE_ADDED) {
return type.toString();
}
return type + ": " + table.getName();
}
}
private static class MyDbListener implements DBListener {
private List<DbEvent> events = new ArrayList<>();
@Override
public void dbRestored(DBHandle dbh) {
events.add(new DbEvent(DbNotifyType.RESTORED, null));
}
@Override
public void dbClosed(DBHandle dbh) {
events.add(new DbEvent(DbNotifyType.CLOSED, null));
}
@Override
public void tableDeleted(DBHandle dbh, Table table) {
events.add(new DbEvent(DbNotifyType.TABLE_DELETED, table));
}
@Override
public void tableAdded(DBHandle dbh, Table table) {
events.add(new DbEvent(DbNotifyType.TABLE_ADDED, table));
}
}
} }

View file

@ -101,6 +101,13 @@ abstract class AbstractTransactionManager {
return null; return null;
} }
/**
* Force transaction lock and terminate current transaction.
* @param rollback true if rollback of non-commited changes should occurs, false if commit
* should be done. NOTE: it can be potentially detrimental to commit an incomplete transaction
* and should be avoided.
* @param reason very short reason for requesting lock
*/
final void forceLock(boolean rollback, String reason) { final void forceLock(boolean rollback, String reason) {
synchronized (this) { synchronized (this) {
@ -119,6 +126,13 @@ abstract class AbstractTransactionManager {
terminateTransaction(rollback, true); terminateTransaction(rollback, true);
} }
/**
* Terminate current transaction.
* @param rollback true if rollback of non-commited changes should occurs, false if commit
* should be done. NOTE: it can be potentially detrimental to commit an incomplete transaction
* and should be avoided.
* @param notify true for listeners to be notified else false
*/
abstract void terminateTransaction(boolean rollback, boolean notify); abstract void terminateTransaction(boolean rollback, boolean notify);
final synchronized void unlock() { final synchronized void unlock() {

View file

@ -284,8 +284,10 @@ public interface DomainObject {
public boolean lock(String reason); public boolean lock(String reason);
/** /**
* Cancels any previous lock and acquires it. * Force transaction lock and terminate current transaction.
* @param rollback if true, any changes in made with the previous lock should be discarded. * @param rollback true if rollback of non-commited changes should occurs, false if commit
* should be done. NOTE: it can be potentially detrimental to commit an incomplete transaction
* which should be avoided.
* @param reason very short reason for requesting lock * @param reason very short reason for requesting lock
*/ */
public void forceLock(boolean rollback, String reason); public void forceLock(boolean rollback, String reason);