mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-21 07:09:56 +01:00
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:
@@ -68,6 +68,13 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
void close() throws DbException;
|
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.
|
* Starts a new transaction and returns an object representing it.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,4 +37,10 @@ interface DatabaseConstants {
|
|||||||
* has passed since the last compaction.
|
* has passed since the last compaction.
|
||||||
*/
|
*/
|
||||||
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,14 @@ class H2Database extends JdbcDatabase {
|
|||||||
@Override
|
@Override
|
||||||
public void close() throws DbException {
|
public void close() throws DbException {
|
||||||
// H2 will close the database when the last connection closes
|
// H2 will close the database when the last connection closes
|
||||||
|
Connection c = null;
|
||||||
try {
|
try {
|
||||||
|
c = createConnection();
|
||||||
super.closeAllConnections();
|
super.closeAllConnections();
|
||||||
|
setDirty(c, false);
|
||||||
|
c.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
tryToClose(c, LOG, WARNING);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class HyperSqlDatabase extends JdbcDatabase {
|
|||||||
try {
|
try {
|
||||||
super.closeAllConnections();
|
super.closeAllConnections();
|
||||||
c = createConnection();
|
c = createConnection();
|
||||||
|
setDirty(c, false);
|
||||||
s = c.createStatement();
|
s = c.createStatement();
|
||||||
s.executeQuery("SHUTDOWN");
|
s.executeQuery("SHUTDOWN");
|
||||||
s.close();
|
s.close();
|
||||||
|
|||||||
@@ -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.PENDING;
|
||||||
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
|
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.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.LAST_COMPACTED_KEY;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
|
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
||||||
@@ -354,9 +355,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
@GuardedBy("connectionsLock")
|
@GuardedBy("connectionsLock")
|
||||||
private boolean closed = false;
|
private boolean closed = false;
|
||||||
|
|
||||||
|
private volatile boolean wasDirtyOnInitialisation = false;
|
||||||
|
|
||||||
protected abstract Connection createConnection()
|
protected abstract Connection createConnection()
|
||||||
throws DbException, SQLException;
|
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;
|
protected abstract void compactAndClose() throws DbException;
|
||||||
|
|
||||||
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
|
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
|
||||||
@@ -381,13 +387,19 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
if (reopen) {
|
if (reopen) {
|
||||||
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||||
|
wasDirtyOnInitialisation = isDirty(s);
|
||||||
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
|
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
|
||||||
} else {
|
} else {
|
||||||
|
wasDirtyOnInitialisation = false;
|
||||||
createTables(txn);
|
createTables(txn);
|
||||||
initialiseSettings(txn);
|
initialiseSettings(txn);
|
||||||
compact = false;
|
compact = false;
|
||||||
}
|
}
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("db dirty? " + wasDirtyOnInitialisation);
|
||||||
|
}
|
||||||
createIndexes(txn);
|
createIndexes(txn);
|
||||||
|
setDirty(txn, true);
|
||||||
commitTransaction(txn);
|
commitTransaction(txn);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
abortTransaction(txn);
|
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
|
* Compares the schema version stored in the database with the schema
|
||||||
* version used by the current code and applies any suitable migrations to
|
* 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);
|
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 {
|
private void initialiseSettings(Connection txn) throws DbException {
|
||||||
Settings s = new Settings();
|
Settings s = new Settings();
|
||||||
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
|
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.Driver;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
@@ -2347,6 +2351,67 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
db.close();
|
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 {
|
private Database<Connection> open(boolean resume) throws Exception {
|
||||||
return open(resume, new TestMessageFactory(), new SystemClock());
|
return open(resume, new TestMessageFactory(), new SystemClock());
|
||||||
}
|
}
|
||||||
@@ -2402,6 +2467,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
rootKey, alice);
|
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
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
deleteTestDirectory(testDir);
|
deleteTestDirectory(testDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user