Implement remove-before-add message sequence behavior

It is possible to receive a RemoveData or RemoveMailboxData message
before the relevant AddData, but the current code does not handle
it.

This results in internal state updates and signal handler's being called
when an Add is received with a lower sequence number than a previously
seen Remove.

Minor test validation changes to allow tests to specify that only the
SequenceNumberMap should be written during an operation.
This commit is contained in:
Julian Knutsen 2019-11-19 16:22:53 -08:00
parent 526aee5ed4
commit 372c26de74
No known key found for this signature in database
GPG key ID: D85F536DB3615B2D
3 changed files with 48 additions and 22 deletions

View file

@ -482,13 +482,6 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload();
ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload);
// If we don't know about the target of this remove, ignore it
ProtectedStorageEntry storedEntry = map.get(hashOfPayload);
if (storedEntry == null) {
log.debug("Remove data ignored as we don't have an entry for that data.");
return false;
}
// If we have seen a more recent operation for this payload, ignore this one // If we have seen a more recent operation for this payload, ignore this one
if (!hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload)) if (!hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload))
return false; return false;
@ -498,19 +491,26 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
return false; return false;
// If we have already seen an Entry with the same hash, verify the metadata is the same // If we have already seen an Entry with the same hash, verify the metadata is the same
if (!protectedStorageEntry.matchesRelevantPubKey(storedEntry)) ProtectedStorageEntry storedEntry = map.get(hashOfPayload);
if (storedEntry != null && !protectedStorageEntry.matchesRelevantPubKey(storedEntry))
return false; return false;
// Valid remove entry, do the remove and signal listeners
removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload);
printData("after remove");
// Record the latest sequence number and persist it // Record the latest sequence number and persist it
sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis())); sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis()));
sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 300); sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 300);
maybeAddToRemoveAddOncePayloads(protectedStoragePayload, hashOfPayload); maybeAddToRemoveAddOncePayloads(protectedStoragePayload, hashOfPayload);
// This means the RemoveData or RemoveMailboxData was seen prior to the AddData. We have already updated
// the SequenceNumberMap appropriately so the stale Add will not pass validation, but we don't want to
// signal listeners for state changes since no original state existed.
if (storedEntry == null)
return false;
// Valid remove entry, do the remove and signal listeners
removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload);
printData("after remove");
if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) {
broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null, isDataOwner); broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null, isDataOwner);
} else { } else {

View file

@ -188,7 +188,7 @@ public class P2PDataStorageClientAPITest {
SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry);
Assert.assertFalse(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), true)); Assert.assertFalse(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), true));
this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, false, true, false, true); this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, false, true, true, true);
} }
// TESTCASE: Adding, then removing a mailbox message from the getMailboxDataWithSignedSeqNr API // TESTCASE: Adding, then removing a mailbox message from the getMailboxDataWithSignedSeqNr API

View file

@ -204,6 +204,14 @@ public class P2PDataStorageProtectedStorageEntryTest {
boolean expectedReturnValue, boolean expectedReturnValue,
boolean expectInternalStateChange) { boolean expectInternalStateChange) {
doProtectedStorageRemoveAndVerify(entry, expectedReturnValue, expectInternalStateChange, expectInternalStateChange);
}
void doProtectedStorageRemoveAndVerify(ProtectedStorageEntry entry,
boolean expectedReturnValue,
boolean expectInternalStateChange,
boolean expectSeqNrWrite) {
SavedTestState beforeState = this.testState.saveTestState(entry); SavedTestState beforeState = this.testState.saveTestState(entry);
boolean addResult = this.doRemove(entry); boolean addResult = this.doRemove(entry);
@ -211,7 +219,7 @@ public class P2PDataStorageProtectedStorageEntryTest {
if (!this.useMessageHandler) if (!this.useMessageHandler)
Assert.assertEquals(expectedReturnValue, addResult); Assert.assertEquals(expectedReturnValue, addResult);
this.testState.verifyProtectedStorageRemove(beforeState, entry, expectInternalStateChange, true, expectInternalStateChange, this.expectIsDataOwner()); this.testState.verifyProtectedStorageRemove(beforeState, entry, expectInternalStateChange, true, expectSeqNrWrite, this.expectIsDataOwner());
} }
/// Valid Add Tests (isValidForAdd() and matchesRelevantPubKey() return true) /// Valid Add Tests (isValidForAdd() and matchesRelevantPubKey() return true)
@ -325,12 +333,12 @@ public class P2PDataStorageProtectedStorageEntryTest {
doProtectedStorageRemoveAndVerify(entryForRemove, true, true); doProtectedStorageRemoveAndVerify(entryForRemove, true, true);
} }
// TESTCASE: Removing an item before it was added // TESTCASE: Removing an item before it was added. This triggers a SequenceNumberMap write, but nothing else
@Test @Test
public void remove_notExists() { public void remove_notExists() {
ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1);
doProtectedStorageRemoveAndVerify(entryForRemove, false, false); doProtectedStorageRemoveAndVerify(entryForRemove, false, false, true);
} }
// TESTCASE: Removing an item after successfully adding (remove seq # < add seq #) // TESTCASE: Removing an item after successfully adding (remove seq # < add seq #)
@ -415,17 +423,35 @@ public class P2PDataStorageProtectedStorageEntryTest {
} }
// TESTCASE: Received remove for nonexistent item that was later received // TESTCASE: Received remove for nonexistent item that was later received
// XXXBUGXXX: There may be cases where removes are reordered with adds (remove during pending GetDataRequest?).
// The proper behavior may be to not add the late messages, but the current code will successfully add them
// even in the AddOncePayload (mailbox) case.
@Test @Test
public void remove_lateAdd() { public void remove_lateAdd() {
ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1);
ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2);
doProtectedStorageRemoveAndVerify(entryForRemove, false, false); this.doRemove(entryForRemove);
doProtectedStorageAddAndVerify(entryForAdd, false, false);
}
// TESTCASE: Invalid remove doesn't block a valid add (isValidForRemove == false | matchesRelevantPubKey == false)
@Test
public void remove_entryNotIsValidForRemoveDoesntBlockAdd1() {
ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1);
ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1, false, false);
this.doRemove(entryForRemove);
doProtectedStorageAddAndVerify(entryForAdd, true, true);
}
// TESTCASE: Invalid remove doesn't block a valid add (isValidForRemove == false | matchesRelevantPubKey == true)
@Test
public void remove_entryNotIsValidForRemoveDoesntBlockAdd2() {
ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1);
ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1, false, true);
this.doRemove(entryForRemove);
// should be (false, false)
doProtectedStorageAddAndVerify(entryForAdd, true, true); doProtectedStorageAddAndVerify(entryForAdd, true, true);
} }
} }