diff --git a/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java b/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java
index a5aca7aa..6462c353 100644
--- a/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java
+++ b/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java
@@ -10,8 +10,10 @@
public interface TransactionRequesterWorker {
/**
* Works through the request queue by sending a request alongside a random tip to each of our neighbors.
+ *
+ * @return true when we have send the request to our neighbors, otherwise false
*/
- void processRequestQueue();
+ boolean processRequestQueue();
/**
* Starts the background worker that automatically calls {@link #processRequestQueue()} periodically to process the
diff --git a/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java b/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java
index 43d2f1b3..b8e12133 100644
--- a/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java
+++ b/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java
@@ -30,7 +30,7 @@ public class TransactionRequesterWorkerImpl implements TransactionRequesterWorke
/**
* The minimum amount of transactions in the request queue that are required for the worker to trigger.
*/
- private static final int REQUESTER_THREAD_ACTIVATION_THRESHOLD = 50;
+ public static final int REQUESTER_THREAD_ACTIVATION_THRESHOLD = 50;
/**
* The time (in milliseconds) that the worker waits between its iterations.
@@ -105,24 +105,42 @@ public TransactionRequesterWorkerImpl init(Tangle tangle, TransactionRequester t
* traffic like transactions that get relayed by our node.
*/
@Override
- public void processRequestQueue() {
+ public boolean processRequestQueue() {
try {
- if (transactionRequester.numberOfTransactionsToRequest() >= REQUESTER_THREAD_ACTIVATION_THRESHOLD) {
+ if (isActive()) {
TransactionViewModel transaction = getTransactionToSendWithRequest();
- if (transaction != null && transaction.getType() != TransactionViewModel.PREFILLED_SLOT) {
- for (Neighbor neighbor : node.getNeighbors()) {
- try {
- // automatically adds the hash of a requested transaction when sending a packet
- node.sendPacket(transaction, neighbor);
- } catch (Exception e) {
- log.error("unexpected error while sending request to neighbour", e);
- }
- }
+ if (isValidTransaction(transaction)) {
+ sendToNodes(transaction);
+ return true;
}
}
} catch (Exception e) {
log.error("unexpected error while processing the request queue", e);
}
+ return false;
+ }
+
+ private void sendToNodes(TransactionViewModel transaction) {
+ for (Neighbor neighbor : node.getNeighbors()) {
+ try {
+ // automatically adds the hash of a requested transaction when sending a packet
+ node.sendPacket(transaction, neighbor);
+ } catch (Exception e) {
+ log.error("unexpected error while sending request to neighbour", e);
+ }
+ }
+ }
+
+ //@VisibleForTesting
+ boolean isActive() {
+ return transactionRequester.numberOfTransactionsToRequest() >= REQUESTER_THREAD_ACTIVATION_THRESHOLD;
+ }
+
+ //@VisibleForTesting
+ boolean isValidTransaction(TransactionViewModel transaction) {
+ return transaction != null && (
+ transaction.getType() != TransactionViewModel.PREFILLED_SLOT
+ || transaction.getHash().equals(Hash.NULL_HASH));
}
@Override
@@ -145,7 +163,8 @@ public void shutdown() {
* @return a random tip
* @throws Exception if anything unexpected happens while trying to retrieve the random tip.
*/
- private TransactionViewModel getTransactionToSendWithRequest() throws Exception {
+ //@VisibleForTesting
+ TransactionViewModel getTransactionToSendWithRequest() throws Exception {
Hash tip = tipsViewModel.getRandomSolidTipHash();
if (tip == null) {
tip = tipsViewModel.getRandomNonSolidTipHash();
diff --git a/src/test/java/net/helix/hlx/TangleMockUtils.java b/src/test/java/net/helix/hlx/TangleMockUtils.java
new file mode 100644
index 00000000..fe6d5855
--- /dev/null
+++ b/src/test/java/net/helix/hlx/TangleMockUtils.java
@@ -0,0 +1,50 @@
+package net.helix.hlx;
+
+import net.helix.hlx.controllers.TransactionViewModel;
+import net.helix.hlx.model.Hash;
+import net.helix.hlx.model.persistables.Transaction;
+import net.helix.hlx.storage.Tangle;
+import net.helix.hlx.utils.Pair;
+
+import org.mockito.Mockito;
+
+
+public class TangleMockUtils {
+
+ /**
+ * Creates an empty transaction, which is marked filled and parsed.
+ * This transaction is returned when the hash is asked to load in the tangle object
+ *
+ * @param tangle mocked tangle object that shall retrieve a milestone object when being queried for it
+ * @param hash transaction hash
+ * @return The newly created (empty) transaction
+ */
+ public static Transaction mockTransaction(Tangle tangle, Hash hash) {
+ Transaction transaction = new Transaction();
+ transaction.bytes = new byte[0];
+ transaction.type = TransactionViewModel.FILLED_SLOT;
+ transaction.parsed = true;
+
+ return mockTransaction(tangle, hash, transaction);
+ }
+
+ /**
+ * Mocks the tangle object by checking for the hash and returning the transaction.
+ *
+ * @param tangle mocked tangle object that shall retrieve a milestone object when being queried for it
+ * @param hash transaction hash
+ * @param transaction the transaction we send back
+ * @return The transaction
+ */
+ public static Transaction mockTransaction(Tangle tangle, Hash hash, Transaction transaction) {
+ try {
+ Mockito.when(tangle.load(Transaction.class, hash)).thenReturn(transaction);
+ Mockito.when(tangle.getLatest(Transaction.class, Hash.class)).thenReturn(new Pair<>(hash, transaction));
+ } catch (Exception e) {
+ // the exception can not be raised since we mock
+ }
+
+ return transaction;
+ }
+
+}
diff --git a/src/test/java/net/helix/hlx/TransactionTestUtils.java b/src/test/java/net/helix/hlx/TransactionTestUtils.java
index a6f93d13..1f3e9161 100644
--- a/src/test/java/net/helix/hlx/TransactionTestUtils.java
+++ b/src/test/java/net/helix/hlx/TransactionTestUtils.java
@@ -1,46 +1,82 @@
-package net.helix.hlx;
-
-import java.util.Random;
-
-import net.helix.hlx.controllers.TransactionViewModel;
-import net.helix.hlx.crypto.SpongeFactory;
-import net.helix.hlx.model.Hash;
-import net.helix.hlx.model.TransactionHash;
-import org.apache.commons.lang3.StringUtils;
-import org.bouncycastle.util.encoders.Hex;
-
-public class TransactionTestUtils {
-
- private static final Random RND = new Random();
-
- /**
- * Generates a transaction with the provided hex string.
- * Transaction hash is calculated and added.
- *
- * @param hex The transaction hex to use
- * @return The transaction
- */
- public static TransactionViewModel createTransactionWithHex(String hex) {
- byte[] hbytes = Hex.decode(hex);
- return new TransactionViewModel(hbytes, TransactionHash.calculate(SpongeFactory.Mode.S256, hbytes));
- }
-
- /**
- * @param hex The hex to change.
- * @return The changed hex
- */
- public static String nextWord(String hex, int index) {
- return pad(Integer.toHexString(index+1));
- }
-
- private static String pad(String hex) {
- return StringUtils.rightPad(hex, TransactionViewModel.SIZE*2, '0');
- }
-
- public static Hash getTransactionHash() {
- byte[] bytes = new byte[Hash.SIZE_IN_BYTES];
- RND.nextBytes(bytes);
- return net.helix.hlx.model.HashFactory.TRANSACTION.create(bytes);
- }
-
-}
+package net.helix.hlx;
+
+import java.util.Random;
+
+import net.helix.hlx.controllers.TransactionViewModel;
+import net.helix.hlx.crypto.SpongeFactory;
+import net.helix.hlx.model.Hash;
+import net.helix.hlx.model.TransactionHash;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.util.encoders.Hex;
+
+public class TransactionTestUtils {
+
+ private static final Random RND = new Random();
+
+ /**
+ * Generates a transaction with the provided hex string.
+ * Transaction hash is calculated and added.
+ *
+ * @param hex The transaction hex to use
+ * @return The transaction
+ */
+ public static TransactionViewModel createTransactionWithHex(String hex) {
+ byte[] hbytes = Hex.decode(hex);
+ return new TransactionViewModel(hbytes, TransactionHash.calculate(SpongeFactory.Mode.S256, hbytes));
+ }
+
+ /**
+ * @param hex The hex to change.
+ * @return The changed hex
+ */
+ public static String nextWord(String hex, int index) {
+ return pad(Integer.toHexString(index+1));
+ }
+
+ private static String pad(String hex) {
+ return StringUtils.rightPad(hex, TransactionViewModel.SIZE*2, '0');
+ }
+
+ public static Hash getTransactionHash() {
+ byte[] bytes = new byte[Hash.SIZE_IN_BYTES];
+ RND.nextBytes(bytes);
+ return net.helix.hlx.model.HashFactory.TRANSACTION.create(bytes);
+ }
+
+ /**
+ * Generates a transaction.
+ *
+ * @return The transaction
+ */
+ public static Transaction getTransaction() {
+ byte[] bytes = new byte[TransactionViewModel.SIZE];
+ RND.nextBytes(bytes);
+ return buildTransaction(bytes);
+ }
+
+ /**
+ * Generates a transaction with only 0s.
+ *
+ * @return The transaction
+ */
+ public static Transaction get0Transaction() {
+ byte[] bytes = new byte[TransactionViewModel.SIZE];
+ return buildTransaction(bytes);
+ }
+
+ /**
+ * Builds a transaction from the bytes.
+ * Make sure the bytes are in the correct order
+ *
+ * @param bytes The bytes to build the transaction
+ * @return The created transaction
+ */
+ public static Transaction buildTransaction(byte[] bytes) {
+ Transaction t = new Transaction();
+ t.read(bytes);
+ t.readMetadata(bytes);
+ return t;
+ }
+
+}
diff --git a/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java b/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java
new file mode 100644
index 00000000..b2b70082
--- /dev/null
+++ b/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java
@@ -0,0 +1,148 @@
+package net.helix.hlx.network.impl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import static org.mockito.Mockito.when;
+
+import net.helix.hlx.controllers.TipsViewModel;
+import net.helix.hlx.controllers.TransactionViewModel;
+import net.helix.hlx.model.Hash;
+import net.helix.hlx.model.persistables.Transaction;
+import net.helix.hlx.network.Node;
+import net.helix.hlx.network.TransactionRequester;
+import net.helix.hlx.service.snapshot.SnapshotProvider;
+import net.helix.hlx.storage.Tangle;
+
+import net.helix.hlx.TangleMockUtils;
+import static net.helix.hlx.TransactionTestUtils.getTransaction;
+import static net.helix.hlx.TransactionTestUtils.get0Transaction;
+import static net.helix.hlx.TransactionTestUtils.buildTransaction;
+import static net.helix.hlx.TransactionTestUtils.getTransactionHash;
+
+
+public class TransactionRequesterWorkerImplTest {
+
+ //Good
+ private static final TransactionViewModel TVMRandomNull = new TransactionViewModel(
+ getTransaction(), Hash.NULL_HASH);
+ private static final TransactionViewModel TVMRandomNotNull = new TransactionViewModel(
+ getTransaction(), getTransactionHash());
+ private static final TransactionViewModel TVMAll0Null = new TransactionViewModel(
+ get0Transaction(), Hash.NULL_HASH);
+ private static final TransactionViewModel TVMAll0NotNull = new TransactionViewModel(
+ get0Transaction(), getTransactionHash());
+
+ //Bad
+ private static final TransactionViewModel TVMNullNull = new TransactionViewModel((Transaction)null, Hash.NULL_HASH);
+
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private static SnapshotProvider snapshotProvider;
+
+ private static TransactionRequester requester;
+ private static TransactionRequesterWorkerImpl worker;
+
+ @Mock
+ private Tangle tangle;
+
+ @Mock
+ private Node node;
+
+ @Mock
+ private TipsViewModel tipsVM;
+
+ @Before
+ public void before() {
+ requester = new TransactionRequester(tangle, snapshotProvider);
+
+ worker = new TransactionRequesterWorkerImpl();
+ worker.init(tangle, requester, tipsVM, node);
+ }
+
+ @After
+ public void tearDown() {
+ worker.shutdown();
+ }
+
+ @Test
+ public void workerActive() throws Exception {
+ assertFalse("Empty worker should not be active", worker.isActive());
+
+ fillRequester();
+
+ assertTrue("Worker should be active when it requester is over threshold", worker.isActive());
+ }
+
+ @Test
+ public void processRequestQueueTest() throws Exception {
+ //getTransactionToSendWithRequest starts reading from solid tips, so mock data from that call
+ when(tipsVM.getRandomSolidTipHash()).thenReturn(
+ TVMRandomNull.getHash(),
+ TVMRandomNotNull.getHash(),
+ TVMAll0Null.getHash(),
+ TVMAll0NotNull.getHash(),
+ TVMNullNull.getHash(),
+ null);
+
+ assertFalse("Unfilled queue shouldnt process", worker.processRequestQueue());
+
+ //Requester never goes down since nodes don't really request
+ fillRequester();
+
+ TangleMockUtils.mockTransaction(tangle, TVMRandomNull.getHash(), buildTransaction(TVMRandomNull.getBytes()));
+ assertTrue("Null transaction hash should be processed", worker.processRequestQueue());
+
+ TangleMockUtils.mockTransaction(tangle, TVMRandomNotNull.getHash(), buildTransaction(TVMRandomNotNull.getBytes()));
+ assertTrue("Not null transaction hash should be processed", worker.processRequestQueue());
+
+ TangleMockUtils.mockTransaction(tangle, TVMAll0Null.getHash(), buildTransaction(TVMAll0Null.getBytes()));
+ assertTrue("Null transaction hash should be processed", worker.processRequestQueue());
+
+ TangleMockUtils.mockTransaction(tangle, TVMAll0NotNull.getHash(), buildTransaction(TVMAll0NotNull.getBytes()));
+ assertTrue("All 9s transaction should be processed", worker.processRequestQueue());
+
+ // Null gets loaded as all 0, so type is 0 -> Filled
+ TangleMockUtils.mockTransaction(tangle, TVMNullNull.getHash(), null);
+ assertTrue("0 transaction should be processed", worker.processRequestQueue());
+
+ // null -> NULL_HASH -> gets loaded as all 0 -> filled
+ assertTrue("Null transaction should be processed", worker.processRequestQueue());
+ }
+
+ @Test
+ public void validTipToAddTest() throws Exception {
+ assertTrue("Null transaction hash should always be accepted", worker.isValidTransaction(TVMRandomNull));
+ assertTrue("Not null transaction hash should always be accepted", worker.isValidTransaction(TVMRandomNotNull));
+ assertTrue("Null transaction hash should always be accepted", worker.isValidTransaction(TVMAll0Null));
+ assertTrue("All 9s transaction should be accepted", worker.isValidTransaction(TVMAll0NotNull));
+
+ // Null gets loaded as all 0, so type is 0 -> Filled
+ assertTrue("0 transaction should be accepted", worker.isValidTransaction(TVMNullNull));
+
+ assertFalse("Null transaction should not be accepted", worker.isValidTransaction(null));
+ }
+
+ private void fillRequester() throws Exception {
+ for (int i=0; i< TransactionRequesterWorkerImpl.REQUESTER_THREAD_ACTIVATION_THRESHOLD; i++) {
+ addRequest();
+ }
+ }
+
+ private void addRequest() throws Exception {
+ Hash randomHash = getTransactionHash();
+ TangleMockUtils.mockTransaction(tangle, randomHash);
+ requester.requestTransaction(randomHash, false);
+ }
+
+}