Skip to content

Commit

Permalink
(8/8) Persist changes to ProtectedStoragePayload objects implementing…
Browse files Browse the repository at this point in the history
… PersistablePayload (#3640)

* [PR COMMENTS] Make maxSequenceNumberBeforePurge final

Instead of using a subclass that overwrites a value, utilize Guice
to inject the real value of 10000 in the app and let the tests overwrite
it with their own.

* [TESTS] Clean up 'Analyze Code' warnings

Remove unused imports and clean up some access modifiers now that
the final test structure is complete

* [REFACTOR] HashMapListener::onAdded/onRemoved

Previously, this interface was called each time an item was changed. This
required listeners to understand performance implications of multiple
adds or removes in a short time span.

Instead, give each listener the ability to process a list of added or
removed entrys which can help them avoid performance issues.

This patch is just a refactor. Each listener is called once for each
ProtectedStorageEntry. Future patches will change this.

* [REFACTOR] removeFromMapAndDataStore can operate on Collections

Minor performance overhead for constructing MapEntry and Collections
of one element, but keeps the code cleaner and all removes can still
use the same logic to remove from map, delete from data store, signal
listeners, etc.

The MapEntry type is used instead of Pair since it will require less
operations when this is eventually used in the removeExpiredEntries path.

* Change removeFromMapAndDataStore to signal listeners at the end in a batch

All current users still call this one-at-a-time. But, it gives the ability
for the expire code path to remove in a batch.

* Update removeExpiredEntries to remove all items in a batch

This will cause HashMapChangedListeners to receive just one onRemoved()
call for the expire work instead of multiple onRemoved() calls for each
item.

This required a bit of updating for the remove validation in tests so
that it correctly compares onRemoved with multiple items.

* ProposalService::onProtectedDataRemoved signals listeners once on batch removes

#3143 identified an issue that tempProposals listeners were being
signaled once for each item that was removed during the P2PDataStore
operation that expired old TempProposal objects. Some of the listeners
are very expensive (ProposalListPresentation::updateLists()) which results
in large UI performance issues.

Now that the infrastructure is in place to receive updates from the
P2PDataStore in a batch, the ProposalService can apply all of the removes
received from the P2PDataStore at once. This results in only 1 onChanged()
callback for each listener.

The end result is that updateLists() is only called once and the performance
problems are reduced.

This removes the need for #3148 and those interfaces will be removed in
the next patch.

* Remove HashmapChangedListener::onBatch operations

Now that the only user of this interface has been removed, go ahead
and delete it. This is a partial revert of
f5d75c4 that includes the code that was
added into ProposalService that subscribed to the P2PDataStore.

* [TESTS] Regression test for #3629

Write a test that shows the incorrect behavior for #3629, the hashmap
is rebuilt from disk using the 20-byte key instead of the 32-byte key.

* [BUGFIX] Reconstruct HashMap using 32-byte key

Addresses the first half of #3629 by ensuring that the reconstructed
HashMap always has the 32-byte key for each payload.

It turns out, the TempProposalStore persists the ProtectedStorageEntrys
on-disk as a List and doesn't persist the key at all. Then, on
reconstruction, it creates the 20-byte key for its internal map.

The fix is to update the TempProposalStore to use the 32-byte key instead.
This means that all writes, reads, and reconstrution of the TempProposalStore
uses the 32-byte key which matches perfectly with the in-memory map
of the P2PDataStorage that expects 32-byte keys.

Important to note that until all seednodes receive this update, nodes
will continue to have both the 20-byte and 32-byte keys in their HashMap.

* [BUGFIX] Use 32-byte key in requestData path

Addresses the second half of #3629 by using the HashMap, not the
protectedDataStore to generate the known keys in the requestData path.

This won't have any bandwidth reduction until all seednodes have the
update and only have the 32-byte key in their HashMap.

fixes #3629

* [DEAD CODE] Remove getProtectedDataStoreMap

The only user has been migrated to getMap(). Delete it so future
development doesn't have the same 20-byte vs 32-byte key issue.

* [TESTS] Allow tests to validate SequenceNumberMap write separately

In order to implement remove-before-add behavior, we need a way to
verify that the SequenceNumberMap was the only item updated.

* 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.

* [TESTS] Allow remove() verification to be more flexible

Now that we have introduced remove-before-add, we need a way
to validate that the SequenceNumberMap was written, but nothing
else. Add this feature to the validation path.

* Broadcast remove-before-add messages to P2P network

In order to aid in propagation of remove() messages, broadcast them
in the event the remove is seen before the add.

* [TESTS] Clean up remove verification helpers

Now that there are cases where the SequenceNumberMap and Broadcast
are called, but no other internal state is updated, the existing helper
functions conflate too many decisions. Remove them in favor of explicitly
defining each state change expected.

* [BUGFIX] Fix duplicate sequence number use case (startup)

Fix a bug introduced in d484617 that
did not properly handle a valid use case for duplicate sequence numbers.

For in-memory-only ProtectedStoragePayloads, the client nodes need a way
to reconstruct the Payloads after startup from peer and seed nodes. This
involves sending a ProtectedStorageEntry with a sequence number that
is equal to the last one the client had already seen.

This patch adds tests to confirm the bug and fix as well as the changes
necessary to allow adding of Payloads that were previously seen, but
removed during a restart.

* Clean up AtomicBoolean usage in FileManager

Although the code was correct, it was hard to understand the relationship
between the to-be-written object and the savePending flag.

Trade two dependent atomics for one and comment the code to make it more
clear for the next reader.

* [DEADCODE] Clean up FileManager.java

* [BUGFIX] Shorter delay values not taking precedence

Fix a bug in the FileManager where a saveLater called with a low delay
won't execute until the delay specified by a previous saveLater call.

The trade off here is the execution of a task that returns early vs.
losing the requested delay.

* [REFACTOR] Inline saveNowInternal

Only one caller after deadcode removal.

* [TESTS] Introduce MapStoreServiceFake

Now that we want to make changes to the MapStoreService,
it isn't sufficient to have a Fake of the ProtectedDataStoreService.

Tests now use a REAL ProtectedDataStoreService and a FAKE MapStoreService
to exercise more of the production code and allow future testing of
changes to MapStoreService.

* Persist changes to ProtectedStorageEntrys

With the addition of ProtectedStorageEntrys, there are now persistable
maps that have different payloads and the same keys. In the
ProtectedDataStoreService case, the value is the ProtectedStorageEntry
which has a createdTimeStamp, sequenceNumber, and signature that can
all change, but still contain an identical payload.

Previously, the service was only updating the on-disk representation on
the first object and never again. So, when it was recreated from disk it
would not have any of the updated metadata. This was just copied from the
append-only implementation where the value was the Payload
which was immutable.

This hasn't caused any issues to this point, but it causes strange behavior
such as always receiving seqNr==1 items from seednodes on startup. It
is good practice to keep the in-memory objects and on-disk objects in
sync and removes an unexpected failure in future dev work that expects
the same behavior as the append-only on-disk objects.

* [DEADCODE] Remove protectedDataStoreListener

There were no users.

* [DEADCODE] Remove unused methods in ProtectedDataStoreService
  • Loading branch information
ripcurlx authored Nov 26, 2019
2 parents c705853 + 44a11a0 commit b15eb70
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 105 deletions.
29 changes: 5 additions & 24 deletions p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService;
import bisq.network.p2p.storage.persistence.ProtectedDataStoreListener;
import bisq.network.p2p.storage.persistence.ProtectedDataStoreService;
import bisq.network.p2p.storage.persistence.ResourceDataStoreService;
import bisq.network.p2p.storage.persistence.SequenceNumberMap;
Expand Down Expand Up @@ -126,7 +125,6 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
final SequenceNumberMap sequenceNumberMap = new SequenceNumberMap();

private final Set<AppendOnlyDataStoreListener> appendOnlyDataStoreListeners = new CopyOnWriteArraySet<>();
private final Set<ProtectedDataStoreListener> protectedDataStoreListeners = new CopyOnWriteArraySet<>();
private final Clock clock;

/// The maximum number of items that must exist in the SequenceNumberMap before it is scheduled for a purge
Expand Down Expand Up @@ -426,12 +424,9 @@ public boolean addProtectedStorageEntry(ProtectedStorageEntry protectedStorageEn
if (allowBroadcast)
broadcastProtectedStorageEntry(protectedStorageEntry, sender, listener, isDataOwner);

// Persist ProtectedStorageEntrys carrying PersistablePayload payloads and signal listeners on changes
if (protectedStoragePayload instanceof PersistablePayload) {
ProtectedStorageEntry previous = protectedDataStoreService.putIfAbsent(hashOfPayload, protectedStorageEntry);
if (previous == null)
protectedDataStoreListeners.forEach(e -> e.onAdded(protectedStorageEntry));
}
// Persist ProtectedStorageEntrys carrying PersistablePayload payloads
if (protectedStoragePayload instanceof PersistablePayload)
protectedDataStoreService.put(hashOfPayload, protectedStorageEntry);

return true;
}
Expand Down Expand Up @@ -632,17 +627,6 @@ public void removeAppendOnlyDataStoreListener(AppendOnlyDataStoreListener listen
appendOnlyDataStoreListeners.remove(listener);
}

@SuppressWarnings("unused")
public void addProtectedDataStoreListener(ProtectedDataStoreListener listener) {
protectedDataStoreListeners.add(listener);
}

@SuppressWarnings("unused")
public void removeProtectedDataStoreListener(ProtectedDataStoreListener listener) {
protectedDataStoreListeners.remove(listener);
}


///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -668,11 +652,8 @@ private void removeFromMapAndDataStore(
ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload();
if (protectedStoragePayload instanceof PersistablePayload) {
ProtectedStorageEntry previous = protectedDataStoreService.remove(hashOfPayload, protectedStorageEntry);
if (previous != null) {
protectedDataStoreListeners.forEach(e -> e.onRemoved(protectedStorageEntry));
} else {
log.info("We cannot remove the protectedStorageEntry from the persistedEntryMap as it does not exist.");
}
if (previous == null)
log.error("We cannot remove the protectedStorageEntry from the persistedEntryMap as it does not exist.");
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public MapStoreService(File storageDir, Storage<T> storage) {

public abstract boolean canHandle(R payload);

void put(P2PDataStorage.ByteArray hash, R payload) {
getMap().put(hash, payload);
persist();
}

R putIfAbsent(P2PDataStorage.ByteArray hash, R payload) {
R previous = getMap().putIfAbsent(hash, payload);
persist();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,10 @@ public void put(P2PDataStorage.ByteArray hash, ProtectedStorageEntry entry) {
services.stream()
.filter(service -> service.canHandle(entry))
.forEach(service -> {
service.putIfAbsent(hash, entry);
service.put(hash, entry);
});
}

public ProtectedStorageEntry putIfAbsent(P2PDataStorage.ByteArray hash, ProtectedStorageEntry entry) {
Map<P2PDataStorage.ByteArray, ProtectedStorageEntry> map = getMap();
if (!map.containsKey(hash)) {
put(hash, entry);
return null;
} else {
return map.get(hash);
}
}

public boolean containsKey(P2PDataStorage.ByteArray hash) {
return getMap().containsKey(hash);
}

public ProtectedStorageEntry remove(P2PDataStorage.ByteArray hash, ProtectedStorageEntry protectedStorageEntry) {
final ProtectedStorageEntry[] result = new ProtectedStorageEntry[1];
services.stream()
Expand Down
36 changes: 8 additions & 28 deletions p2p/src/test/java/bisq/network/p2p/storage/TestState.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,12 @@
import bisq.network.p2p.storage.messages.RemoveMailboxDataMessage;
import bisq.network.p2p.storage.mocks.AppendOnlyDataStoreServiceFake;
import bisq.network.p2p.storage.mocks.ClockFake;
import bisq.network.p2p.storage.mocks.ProtectedDataStoreServiceFake;
import bisq.network.p2p.storage.mocks.MapStoreServiceFake;
import bisq.network.p2p.storage.payload.MailboxStoragePayload;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener;
import bisq.network.p2p.storage.persistence.ProtectedDataStoreListener;
import bisq.network.p2p.storage.persistence.ProtectedDataStoreService;
import bisq.network.p2p.storage.persistence.ResourceDataStoreService;
import bisq.network.p2p.storage.persistence.SequenceNumberMap;
Expand Down Expand Up @@ -70,7 +69,6 @@ public class TestState {
final Broadcaster mockBroadcaster;

final AppendOnlyDataStoreListener appendOnlyDataStoreListener;
private final ProtectedDataStoreListener protectedDataStoreListener;
private final HashMapChangedListener hashMapChangedListener;
private final Storage<SequenceNumberMap> mockSeqNrStorage;
private final ProtectedDataStoreService protectedDataStoreService;
Expand All @@ -80,7 +78,7 @@ public class TestState {
this.mockBroadcaster = mock(Broadcaster.class);
this.mockSeqNrStorage = mock(Storage.class);
this.clockFake = new ClockFake();
this.protectedDataStoreService = new ProtectedDataStoreServiceFake();
this.protectedDataStoreService = new ProtectedDataStoreService();

this.mockedStorage = new P2PDataStorage(mock(NetworkNode.class),
this.mockBroadcaster,
Expand All @@ -89,17 +87,16 @@ this.protectedDataStoreService, mock(ResourceDataStoreService.class),
this.mockSeqNrStorage, this.clockFake, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE);

this.appendOnlyDataStoreListener = mock(AppendOnlyDataStoreListener.class);
this.protectedDataStoreListener = mock(ProtectedDataStoreListener.class);
this.hashMapChangedListener = mock(HashMapChangedListener.class);
this.protectedDataStoreService.addService(new MapStoreServiceFake());

this.mockedStorage = createP2PDataStorageForTest(
this.mockBroadcaster,
this.protectedDataStoreService,
this.mockSeqNrStorage,
this.clockFake,
this.hashMapChangedListener,
this.appendOnlyDataStoreListener,
this.protectedDataStoreListener);
this.appendOnlyDataStoreListener);
}


Expand All @@ -118,8 +115,7 @@ void simulateRestart() {
this.mockSeqNrStorage,
this.clockFake,
this.hashMapChangedListener,
this.appendOnlyDataStoreListener,
this.protectedDataStoreListener);
this.appendOnlyDataStoreListener);
}

private static P2PDataStorage createP2PDataStorageForTest(
Expand All @@ -128,8 +124,7 @@ private static P2PDataStorage createP2PDataStorageForTest(
Storage<SequenceNumberMap> sequenceNrMapStorage,
ClockFake clock,
HashMapChangedListener hashMapChangedListener,
AppendOnlyDataStoreListener appendOnlyDataStoreListener,
ProtectedDataStoreListener protectedDataStoreListener) {
AppendOnlyDataStoreListener appendOnlyDataStoreListener) {

P2PDataStorage p2PDataStorage = new P2PDataStorage(mock(NetworkNode.class),
broadcaster,
Expand All @@ -143,15 +138,13 @@ protectedDataStoreService, mock(ResourceDataStoreService.class),

p2PDataStorage.addHashMapChangedListener(hashMapChangedListener);
p2PDataStorage.addAppendOnlyDataStoreListener(appendOnlyDataStoreListener);
p2PDataStorage.addProtectedDataStoreListener(protectedDataStoreListener);

return p2PDataStorage;
}

private void resetState() {
reset(this.mockBroadcaster);
reset(this.appendOnlyDataStoreListener);
reset(this.protectedDataStoreListener);
reset(this.hashMapChangedListener);
reset(this.mockSeqNrStorage);
}
Expand Down Expand Up @@ -219,17 +212,8 @@ void verifyProtectedStorageAdd(SavedTestState beforeState,
if (expectedStateChange) {
Assert.assertEquals(protectedStorageEntry, this.mockedStorage.getMap().get(hashMapHash));

// PersistablePayload payloads need to be written to disk and listeners signaled... unless the hash already exists in the protectedDataStore.
// Note: this behavior is different from the HashMap listeners that are signaled on an increase in seq #, even if the hash already exists.
// TODO: Should the behavior be identical between this and the HashMap listeners?
// TODO: Do we want ot overwrite stale values in order to persist updated sequence numbers and timestamps?
if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload && beforeState.protectedStorageEntryBeforeOpDataStoreMap == null) {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload)
Assert.assertEquals(protectedStorageEntry, this.protectedDataStoreService.getMap().get(hashMapHash));
verify(this.protectedDataStoreListener).onAdded(protectedStorageEntry);
} else {
Assert.assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, this.protectedDataStoreService.getMap().get(hashMapHash));
verify(this.protectedDataStoreListener, never()).onAdded(protectedStorageEntry);
}

verify(this.hashMapChangedListener).onAdded(Collections.singletonList(protectedStorageEntry));

Expand All @@ -250,7 +234,6 @@ void verifyProtectedStorageAdd(SavedTestState beforeState,

// Internal state didn't change... nothing should be notified
verify(this.hashMapChangedListener, never()).onAdded(Collections.singletonList(protectedStorageEntry));
verify(this.protectedDataStoreListener, never()).onAdded(protectedStorageEntry);
verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong());
}
}
Expand Down Expand Up @@ -291,7 +274,6 @@ void verifyProtectedStorageRemove(SavedTestState beforeState,
Assert.assertEquals(expected, actual);
} else {
verify(this.hashMapChangedListener, never()).onRemoved(any());
verify(this.protectedDataStoreListener, never()).onAdded(any());
}

if (!expectedSeqNrWrite)
Expand Down Expand Up @@ -319,11 +301,9 @@ void verifyProtectedStorageRemove(SavedTestState beforeState,
if (expectedHashMapAndDataStoreUpdated) {
Assert.assertNull(this.mockedStorage.getMap().get(hashMapHash));

if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload) {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload)
Assert.assertNull(this.protectedDataStoreService.getMap().get(hashMapHash));

verify(this.protectedDataStoreListener).onRemoved(protectedStorageEntry);
}
} else {
Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp, this.mockedStorage.getMap().get(hashMapHash));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,52 @@

import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.persistence.ProtectedDataStoreService;
import bisq.network.p2p.storage.persistence.MapStoreService;

import bisq.common.proto.persistable.PersistableEnvelope;
import bisq.common.proto.persistable.PersistablePayload;
import bisq.common.storage.Storage;

import java.io.File;

import java.util.HashMap;
import java.util.Map;

import lombok.Getter;

import static org.mockito.Mockito.mock;

/**
* Implementation of an in-memory ProtectedDataStoreService that can be used in tests. Removes overhead
* Implementation of an in-memory MapStoreService that can be used in tests. Removes overhead
* involving files, resources, and services for tests that don't need it.
*
* @see <a href="https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs">Reference</a>
*/
public class ProtectedDataStoreServiceFake extends ProtectedDataStoreService {
public class MapStoreServiceFake extends MapStoreService {
@Getter
private final Map<P2PDataStorage.ByteArray, ProtectedStorageEntry> map;

public ProtectedDataStoreServiceFake() {
super();
map = new HashMap<>();
public MapStoreServiceFake() {
super(mock(File.class), mock(Storage.class));
this.map = new HashMap<>();
}

public Map<P2PDataStorage.ByteArray, ProtectedStorageEntry> getMap() {
return map;
@Override
public String getFileName() {
return null;
}

public void put(P2PDataStorage.ByteArray hashAsByteArray, ProtectedStorageEntry entry) {
map.put(hashAsByteArray, entry);
@Override
protected PersistableEnvelope createStore() {
return null;
}
public ProtectedStorageEntry remove(P2PDataStorage.ByteArray hash, ProtectedStorageEntry protectedStorageEntry) {
return map.remove(hash);

@Override
public boolean canHandle(PersistablePayload payload) {
return true;
}

protected void readFromResources(String postFix) {
// do nothing. This Fake only supports in-memory storage.
}
}

0 comments on commit b15eb70

Please sign in to comment.