Merge branch '57-detect-db-durability-failures' into 'master'

Implement dirty flag to detect durability failures

Closes #57

See merge request briar/briar!1424
This commit is contained in:
akwizgran
2021-04-12 13:17:28 +00:00
6 changed files with 136 additions and 0 deletions

View File

@@ -68,6 +68,13 @@ interface Database<T> {
*/
void close() throws DbException;
/**
* Returns true if the dirty flag was set while opening the database,
* indicating that the database has not been shut down properly the last
* time it was closed and some data could be lost.
*/
boolean wasDirtyOnInitialisation();
/**
* Starts a new transaction and returns an object representing it.
*/

View File

@@ -37,4 +37,10 @@ interface DatabaseConstants {
* has passed since the last compaction.
*/
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
/**
* The {@link Settings} key under which the flag is stored indicating
* whether the database is marked as dirty.
*/
String DIRTY_KEY = "dirty";
}

View File

@@ -84,9 +84,14 @@ class H2Database extends JdbcDatabase {
@Override
public void close() throws DbException {
// H2 will close the database when the last connection closes
Connection c = null;
try {
c = createConnection();
super.closeAllConnections();
setDirty(c, false);
c.close();
} catch (SQLException e) {
tryToClose(c, LOG, WARNING);
throw new DbException(e);
}
}

View File

@@ -81,6 +81,7 @@ class HyperSqlDatabase extends JdbcDatabase {
try {
super.closeAllConnections();
c = createConnection();
setDirty(c, false);
s = c.createStatement();
s.executeQuery("SHUTDOWN");
s.close();

View File

@@ -81,6 +81,7 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERE
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
@@ -354,9 +355,14 @@ abstract class JdbcDatabase implements Database<Connection> {
@GuardedBy("connectionsLock")
private boolean closed = false;
private volatile boolean wasDirtyOnInitialisation = false;
protected abstract Connection createConnection()
throws DbException, SQLException;
// Used exclusively during open to compact the database after schema
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
// elapsed
protected abstract void compactAndClose() throws DbException;
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
@@ -381,13 +387,19 @@ abstract class JdbcDatabase implements Database<Connection> {
try {
if (reopen) {
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
wasDirtyOnInitialisation = isDirty(s);
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
} else {
wasDirtyOnInitialisation = false;
createTables(txn);
initialiseSettings(txn);
compact = false;
}
if (LOG.isLoggable(INFO)) {
LOG.info("db dirty? " + wasDirtyOnInitialisation);
}
createIndexes(txn);
setDirty(txn, true);
commitTransaction(txn);
} catch (DbException e) {
abortTransaction(txn);
@@ -414,6 +426,11 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public boolean wasDirtyOnInitialisation() {
return wasDirtyOnInitialisation;
}
/**
* Compares the schema version stored in the database with the schema
* version used by the current code and applies any suitable migrations to
@@ -488,6 +505,16 @@ abstract class JdbcDatabase implements Database<Connection> {
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private boolean isDirty(Settings s) {
return s.getBoolean(DIRTY_KEY, false);
}
protected void setDirty(Connection txn, boolean dirty) throws DbException {
Settings s = new Settings();
s.putBoolean(DIRTY_KEY, dirty);
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private void initialiseSettings(Connection txn) throws DbException {
Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);

View File

@@ -41,8 +41,12 @@ import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -2347,6 +2351,67 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close();
}
@Test
public void testShutdownGracefully() throws Exception {
Database<Connection> db = open(false);
db.close();
open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirty() throws Exception {
Database<Connection> db = open(false);
// We want to simulate a dirty shutdown here which would normally be
// caused by an empty battery or by force closing the Android app.
// As there is no obvious way to simulate this, we're artificially
// causing an SqlException during close() here by unloading the JDBC
// drivers.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
e.printStackTrace();
}
// Reloading drivers to continue so that we're able to work with the
// database again.
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirtyThenGracefully() throws Exception {
Database<Connection> db = open(false);
// Simulating a dirty shutdown here, look at #testShutdownDirty for
// details.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
}
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
db.close();
db = open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
private Database<Connection> open(boolean resume) throws Exception {
return open(resume, new TestMessageFactory(), new SystemClock());
}
@@ -2402,6 +2467,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
rootKey, alice);
}
private List<String> unloadDrivers() {
Enumeration<Driver> drivers = DriverManager.getDrivers();
List<String> unloaded = new ArrayList<>();
while (drivers.hasMoreElements()) {
Driver d = drivers.nextElement();
try {
DriverManager.deregisterDriver(d);
unloaded.add(d.getClass().getName());
} catch (SQLException e) {
e.printStackTrace();
fail();
}
}
return unloaded;
}
private void reloadDrivers(List<String> unloadedDrivers)
throws ClassNotFoundException, IllegalAccessException,
InstantiationException, SQLException {
for (String driverName : unloadedDrivers) {
DriverManager.registerDriver(
(Driver) Class.forName(driverName).newInstance());
}
}
@After
public void tearDown() {
deleteTestDirectory(testDir);