diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index 332da1fae7af113320bdac05037467620bfea93d..b72849d6f9914e3910ab20d41a54167cadeff38b 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -15,13 +15,12 @@ public interface DatabaseComponent { static final long MEGABYTES = 1024L * 1024L; - // FIXME: Some of these should be configurable + // FIXME: These should be configurable static final long MIN_FREE_SPACE = 300L * MEGABYTES; static final long CRITICAL_FREE_SPACE = 100L * MEGABYTES; static final long MAX_BYTES_BETWEEN_SPACE_CHECKS = 5L * MEGABYTES; static final long MAX_MS_BETWEEN_SPACE_CHECKS = 60L * 1000L; // 1 min static final long BYTES_PER_SWEEP = 5L * MEGABYTES; - static final int MS_BETWEEN_SWEEPS = 1000; // 1 sec static final int RETRANSMIT_THRESHOLD = 3; /** diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index 59bf432a51bd1b5929d80204d731dc4c30847182..9965e950d0c118205da151a839319430a5ed0bca 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -153,6 +153,13 @@ interface Database<T> { */ long getFreeSpace() throws DbException; + /** + * Returns the group that contains the given message. + * <p> + * Locking: messages read. + */ + GroupId getGroup(T txn, MessageId m) throws DbException; + /** * Returns the message identified by the given ID. * <p> @@ -168,12 +175,12 @@ interface Database<T> { Iterable<MessageId> getMessagesByAuthor(T txn, AuthorId a) throws DbException; /** - * Returns the IDs of all children of the message identified by the given - * ID that are present in the database. + * Returns the number of children of the message identified by the given + * ID that are present in the database and sendable. * <p> * Locking: messages read. */ - Iterable<MessageId> getMessagesByParent(T txn, MessageId m) throws DbException; + int getNumberOfSendableChildren(T txn, MessageId m) throws DbException; /** * Returns the IDs of the oldest messages in the database, with a total diff --git a/components/net/sf/briar/db/DatabaseCleaner.java b/components/net/sf/briar/db/DatabaseCleaner.java index 342ae6c3ed0f44145c5acd39b27b3d7a1be64cc9..19615e68c1a5bef9b1e51cfefa00c0779aae63ff 100644 --- a/components/net/sf/briar/db/DatabaseCleaner.java +++ b/components/net/sf/briar/db/DatabaseCleaner.java @@ -16,16 +16,17 @@ interface DatabaseCleaner { interface Callback { /** - * Checks how much free storage space is available to the database, and if - * necessary expires old messages until the free space is at least - * MIN_FREE_SPACE. While the free space is less than CRITICAL_FREE_SPACE, - * operations that attempt to store messages in the database will block. + * Checks how much free storage space is available to the database, and + * if necessary expires old messages until the free space is at least + * MIN_FREE_SPACE. While the free space is less than + * CRITICAL_FREE_SPACE, operations that attempt to store messages in + * the database will block. */ void checkFreeSpaceAndClean() throws DbException; /** - * Called by the cleaner; returns true iff the amount of free storage space - * available to the database should be checked. + * Returns true iff the amount of free storage space available to the + * database should be checked. */ boolean shouldCheckFreeSpace(); } diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index 2fe39f973f3cfcca903cb6ef8a19315dc4040881..82bd7156e1550ae011d6ca9dfd36b86842255b54 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -3,9 +3,9 @@ package net.sf.briar.db; import java.util.logging.Level; import java.util.logging.Logger; +import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; @@ -63,11 +63,7 @@ DatabaseCleaner.Callback { // One point for a good rating if(getRating(m.getAuthor()) == Rating.GOOD) sendability++; // One point per sendable child (backward inclusion) - for(MessageId kid : db.getMessagesByParent(txn, m.getId())) { - Integer kidSendability = db.getSendability(txn, kid); - assert kidSendability != null; - if(kidSendability > 0) sendability++; - } + sendability += db.getNumberOfSendableChildren(txn, m.getId()); return sendability; } @@ -195,6 +191,7 @@ DatabaseCleaner.Callback { MessageId parent = db.getParent(txn, m); if(parent.equals(MessageId.NONE)) break; if(!db.containsMessage(txn, parent)) break; + if(!db.getGroup(txn, m).equals(db.getGroup(txn, parent))) break; Integer parentSendability = db.getSendability(txn, parent); assert parentSendability != null; if(increment) { diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index c848cafa2a8f6edbb516522b19b2b6c5979d9052..efb6c67ee74b5c73bdb6538a3a745bd895344fba 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -17,9 +17,9 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; @@ -376,6 +376,16 @@ abstract class JdbcDatabase implements Database<Connection> { int rowsAffected = ps.executeUpdate(); assert rowsAffected == 1; ps.close(); + sql = "INSERT INTO receivedBundles" + + " (bundleId, contactId, timestamp)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, BundleId.NONE.getBytes()); + ps.setInt(2, c.getInt()); + ps.setLong(3, System.currentTimeMillis()); + rowsAffected = ps.executeUpdate(); + assert rowsAffected == 1; + ps.close(); } catch(SQLException e) { tryToClose(ps); tryToClose(txn); @@ -736,6 +746,30 @@ abstract class JdbcDatabase implements Database<Connection> { } else return f.length(); } + public GroupId getGroup(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT groupId FROM messages WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + byte[] group = rs.getBytes(1); + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return new GroupId(group); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public Message getMessage(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; ResultSet rs = null; @@ -792,20 +826,21 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Iterable<MessageId> getMessagesByParent(Connection txn, MessageId m) - throws DbException { + private int getNumberOfMessages(Connection txn) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT messageId FROM messages WHERE parentId = ?"; + String sql = "SELECT COUNT(messageId) FROM messages"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); rs = ps.executeQuery(); - List<MessageId> ids = new ArrayList<MessageId>(); - while(rs.next()) ids.add(new MessageId(rs.getBytes(1))); + boolean found = rs.next(); + assert found; + int count = rs.getInt(1); + boolean more = rs.next(); + assert !more; rs.close(); ps.close(); - return ids; + return count; } catch(SQLException e) { tryToClose(rs); tryToClose(ps); @@ -814,20 +849,37 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public int getNumberOfMessages(Connection txn) throws DbException { + public int getNumberOfSendableChildren(Connection txn, MessageId m) + throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT COUNT(messageId) FROM messages"; + // Children in other groups should not be counted + String sql = "SELECT groupId FROM messages WHERE messageId = ?"; ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); rs = ps.executeQuery(); boolean found = rs.next(); assert found; - int count = rs.getInt(1); + byte[] group = rs.getBytes(1); boolean more = rs.next(); assert !more; rs.close(); ps.close(); + sql = "SELECT COUNT(messageId) FROM messages" + + " WHERE parentId = ? AND groupId = ?" + + " AND sendability > ZERO()"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setBytes(2, group); + rs = ps.executeQuery(); + found = rs.next(); + assert found; + int count = rs.getInt(1); + more = rs.next(); + assert !more; + rs.close(); + ps.close(); return count; } catch(SQLException e) { tryToClose(rs); diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index 98fd93522cd7698462e0ce09e06aacac48641f7e..7b87d0b84b5cad257ad6e7d50ddced3e7228f976 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -18,6 +19,7 @@ import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.BundleId; import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageFactory; @@ -38,28 +40,29 @@ public class H2DatabaseTest extends TestCase { private final File testDir = TestUtils.getTestDirectory(); // The password has the format <file password> <space> <user password> private final String passwordString = "foo bar"; - // Some bytes for test IDs - private final byte[] idBytes = new byte[32], idBytes1 = new byte[32]; + private final Random random; + private final AuthorId authorId; private final BatchId batchId; private final ContactId contactId; - private final MessageId messageId; private final GroupId groupId; - private final AuthorId authorId; - private final long timestamp = System.currentTimeMillis(); - private final int size = 1234; - private final byte[] body = new byte[size]; + private final MessageId messageId; + private final long timestamp; + private final int size; + private final byte[] body; private final Message message; public H2DatabaseTest() { super(); - for(int i = 0; i < idBytes.length; i++) idBytes[i] = (byte) i; - for(int i = 0; i < idBytes1.length; i++) idBytes1[i] = (byte) (i + 1); - for(int i = 0; i < body.length; i++) body[i] = (byte) i; - batchId = new BatchId(idBytes); + random = new Random(); + authorId = new AuthorId(getRandomId()); + batchId = new BatchId(getRandomId()); contactId = new ContactId(123); - messageId = new MessageId(idBytes); - groupId = new GroupId(idBytes); - authorId = new AuthorId(idBytes); + groupId = new GroupId(getRandomId()); + messageId = new MessageId(getRandomId()); + timestamp = System.currentTimeMillis(); + size = 1234; + body = new byte[size]; + random.nextBytes(body); message = new MessageImpl(messageId, MessageId.NONE, groupId, authorId, timestamp, body); } @@ -336,7 +339,7 @@ public class H2DatabaseTest extends TestCase { @Test public void testBatchesToAck() throws DbException { - BatchId batchId1 = new BatchId(idBytes1); + BatchId batchId1 = new BatchId(getRandomId()); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); @@ -457,10 +460,57 @@ public class H2DatabaseTest extends TestCase { context.assertIsSatisfied(); } + @Test + public void testRetransmission() throws DbException { + BundleId bundleId = new BundleId(getRandomId()); + BundleId bundleId1 = new BundleId(getRandomId()); + BundleId bundleId2 = new BundleId(getRandomId()); + BundleId bundleId3 = new BundleId(getRandomId()); + BundleId bundleId4 = new BundleId(getRandomId()); + BatchId batchId1 = new BatchId(getRandomId()); + BatchId batchId2 = new BatchId(getRandomId()); + Set<MessageId> empty = Collections.emptySet(); + Mockery context = new Mockery(); + MessageFactory messageFactory = context.mock(MessageFactory.class); + + // Create a new database + Database<Connection> db = open(false, messageFactory); + // Add a contact + Connection txn = db.startTransaction(); + db.addContact(txn, contactId); + // Add an oustanding batch (associated with BundleId.NONE) + db.addOutstandingBatch(txn, contactId, batchId, empty); + // Receive a bundle + Set<BatchId> lost = db.addReceivedBundle(txn, contactId, bundleId); + assertTrue(lost.isEmpty()); + // Add a couple more outstanding batches (associated with bundleId) + db.addOutstandingBatch(txn, contactId, batchId1, empty); + db.addOutstandingBatch(txn, contactId, batchId2, empty); + // Receive another bundle + lost = db.addReceivedBundle(txn, contactId, bundleId1); + assertTrue(lost.isEmpty()); + // The contact acks one of the batches - it should not be retransmitted + db.removeAckedBatch(txn, contactId, batchId1); + // Receive another bundle - batchId should now be considered lost + lost = db.addReceivedBundle(txn, contactId, bundleId2); + assertEquals(1, lost.size()); + assertTrue(lost.contains(batchId)); + // Receive another bundle - batchId2 should now be considered lost + lost = db.addReceivedBundle(txn, contactId, bundleId3); + assertEquals(1, lost.size()); + assertTrue(lost.contains(batchId2)); + // Receive another bundle - no further losses + lost = db.addReceivedBundle(txn, contactId, bundleId4); + assertTrue(lost.isEmpty()); + db.commitTransaction(txn); + db.close(); + context.assertIsSatisfied(); + } + @Test public void testGetMessagesByAuthor() throws DbException { - AuthorId authorId1 = new AuthorId(idBytes1); - MessageId messageId1 = new MessageId(idBytes1); + AuthorId authorId1 = new AuthorId(getRandomId()); + MessageId messageId1 = new MessageId(getRandomId()); Message message1 = new MessageImpl(messageId1, MessageId.NONE, groupId, authorId1, timestamp, body); Mockery context = new Mockery(); @@ -493,28 +543,44 @@ public class H2DatabaseTest extends TestCase { } @Test - public void testGetMessagesByParent() throws DbException { - MessageId parentId = new MessageId(idBytes1); - Message message1 = new MessageImpl(messageId, parentId, groupId, + public void testGetNumberOfSendableChildren() throws DbException { + MessageId childId1 = new MessageId(getRandomId()); + MessageId childId2 = new MessageId(getRandomId()); + MessageId childId3 = new MessageId(getRandomId()); + GroupId groupId1 = new GroupId(getRandomId()); + Message child1 = new MessageImpl(childId1, messageId, groupId, + authorId, timestamp, body); + Message child2 = new MessageImpl(childId2, messageId, groupId, + authorId, timestamp, body); + // The third child is in a different group + Message child3 = new MessageImpl(childId3, messageId, groupId1, authorId, timestamp, body); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); // Create a new database Database<Connection> db = open(false, messageFactory); - // Subscribe to a group and store a message + // Subscribe to the groups and store the messages Connection txn = db.startTransaction(); db.addSubscription(txn, groupId); - db.addMessage(txn, message1); + db.addSubscription(txn, groupId1); + db.addMessage(txn, message); + db.addMessage(txn, child1); + db.addMessage(txn, child2); + db.addMessage(txn, child3); + // Make all the children sendable + db.setSendability(txn, childId1, 1); + db.setSendability(txn, childId2, 5); + db.setSendability(txn, childId3, 3); db.commitTransaction(txn); - // Check that the message is retrievable via its parent + // There should be two sendable children txn = db.startTransaction(); - Iterator<MessageId> it = - db.getMessagesByParent(txn, parentId).iterator(); - assertTrue(it.hasNext()); - assertEquals(messageId, it.next()); - assertFalse(it.hasNext()); + assertEquals(2, db.getNumberOfSendableChildren(txn, messageId)); + // Make one of the children unsendable + db.setSendability(txn, childId1, 0); + // Now there should be one sendable child + assertEquals(1, db.getNumberOfSendableChildren(txn, messageId)); db.commitTransaction(txn); db.close(); @@ -523,7 +589,7 @@ public class H2DatabaseTest extends TestCase { @Test public void testGetOldMessages() throws DbException { - MessageId messageId1 = new MessageId(idBytes1); + MessageId messageId1 = new MessageId(getRandomId()); Message message1 = new MessageImpl(messageId1, MessageId.NONE, groupId, authorId, timestamp + 1000, body); Mockery context = new Mockery(); @@ -621,7 +687,7 @@ public class H2DatabaseTest extends TestCase { db.commitTransaction(txn); // The other thread should now terminate try { - t.join(10000); + t.join(10 * 1000); } catch(InterruptedException ignored) {} assertTrue(closed.get()); // Check that the other thread didn't encounter an error @@ -693,6 +759,12 @@ public class H2DatabaseTest extends TestCase { TestUtils.deleteTestDirectory(testDir); } + private byte[] getRandomId() { + byte[] b = new byte[32]; + random.nextBytes(b); + return b; + } + private static class TestMessageFactory implements MessageFactory { public Message createMessage(MessageId id, MessageId parent, diff --git a/test/net/sf/briar/util/StringUtilsTest.java b/test/net/sf/briar/util/StringUtilsTest.java index 267414ba2a777d8840b982b2a68a65defa673429..5a379a402e19fbbf07bbacbd258542afe7fe22a2 100644 --- a/test/net/sf/briar/util/StringUtilsTest.java +++ b/test/net/sf/briar/util/StringUtilsTest.java @@ -1,5 +1,7 @@ package net.sf.briar.util; +import java.util.Arrays; + import junit.framework.TestCase; import org.junit.Test; @@ -17,4 +19,27 @@ public class StringUtilsTest extends TestCase { String tail = StringUtils.tail("987654321", 5); assertEquals("...54321", tail); } + + @Test + public void testToHexString() { + byte[] b = new byte[] {1, 2, 3, 127, -128}; + String s = StringUtils.toHexString(b); + assertEquals("0102037F80", s); + } + + @Test + public void testFromHexString() { + try { + StringUtils.fromHexString("12345"); + assertTrue(false); + } catch(IllegalArgumentException expected) {} + try { + StringUtils.fromHexString("ABCDEFGH"); + assertTrue(false); + } catch(IllegalArgumentException expected) {} + byte[] b = StringUtils.fromHexString("0102037F80"); + assertTrue(Arrays.equals(new byte[] {1, 2, 3, 127, -128}, b)); + b = StringUtils.fromHexString("0a0b0c0d0e0f"); + assertTrue(Arrays.equals(new byte[] {10, 11, 12, 13, 14, 15}, b)); + } } diff --git a/util/net/sf/briar/util/StringUtils.java b/util/net/sf/briar/util/StringUtils.java index f61ab476e762e0a1bcb4ea8d4373eb7d87cbc6d4..e99bf14879d2f90721d5a7361fe3385b57a2298f 100644 --- a/util/net/sf/briar/util/StringUtils.java +++ b/util/net/sf/briar/util/StringUtils.java @@ -2,6 +2,11 @@ package net.sf.briar.util; public class StringUtils { + private static final char[] HEX = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + /** * Trims the given string to the given length, returning the head and * appending "..." if the string was trimmed. @@ -19,4 +24,38 @@ public class StringUtils { if(s.length() > length) return "..." + s.substring(s.length() - length); else return s; } + + /** Converts the given raw byte array to a hex string. */ + public static String toHexString(byte[] bytes) { + StringBuilder s = new StringBuilder(bytes.length * 2); + for(byte b : bytes) { + int high = (b >> 4) & 0xF; + s.append(HEX[high]); + int low = b & 0xF; + s.append(HEX[low]); + } + return s.toString(); + } + + /** Converts the given hex string to a raw byte array. */ + public static byte[] fromHexString(String hex) { + int len = hex.length(); + if(len % 2 != 0) throw new IllegalArgumentException("Not a hex string"); + byte[] bytes = new byte[len / 2]; + for(int i = 0, j = 0; i < len; i += 2, j++) { + int high = hexDigitToInt(hex.charAt(i)); + int low = hexDigitToInt(hex.charAt(i + 1)); + int b = (high << 4) + low; + if(b > 127) b -= 256; + bytes[j] = (byte) b; + } + return bytes; + } + + private static int hexDigitToInt(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + throw new IllegalArgumentException("Not a hex digit: " + c); + } }