package org.iota.ict.network; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.iota.ict.Ict; import org.iota.ict.eee.Environment; import org.iota.ict.network.gossip.GossipEvent; import org.iota.ict.utils.*; import org.iota.ict.model.tangle.Tangle; import org.iota.ict.model.transaction.Transaction; import org.iota.ict.utils.properties.FinalProperties; import org.iota.ict.utils.properties.Properties; import java.net.DatagramPacket; import java.util.Comparator; import java.util.Queue; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadLocalRandom; /** * This class sends transactions to neighbors. Together with the {@link Receiver}, they are the two important gateways * for transaction gossip between Ict nodes. Each Ict instance has exactly one {@link Receiver} and one {@link Sender} * to communicate with its neighbors. * <p> * The sending process happens in its own Thread to not block other components. Before being sent, transactions are put * into a {@link #queue}. This class also requests transactions which are not known to the Ict but were referenced by * received transactions either through the branch or trunk. * * @see Ict * @see Receiver */ public class Sender extends RestartableThread implements SenderInterface { private Node node; private final SendingTaskQueue queue = new SendingTaskQueue(); private final Queue<String> transactionsToRequest = new PriorityBlockingQueue<>(); private static final Logger LOGGER = LogManager.getLogger("Sender"); private Properties properties; public Sender(Node node, Properties properties) { super(LOGGER); this.node = node; this.properties = properties; } @Override public void onReceive(GossipEvent event) { Tangle.TransactionLog log = node.ict.getTangle().createTransactionLogIfAbsent(event.getTransaction()); if (!log.wasSent && log.senders.size() < node.neighbors.size()) { log.wasSent = true; queue(event.getTransaction()); } } @Override public Environment getEnvironment() { return Constants.Environments.GOSSIP; } @Override public void run() { while (isRunning()) { if (!queue.isEmpty() && queue.peek().sendingTime <= System.currentTimeMillis()) { sendTransaction(queue.poll().transaction); } else { waitForNextTransaction(); } } } private void waitForNextTransaction() { try { synchronized (queue) { // keep queue.isEmpty() within the synchronized block so notify is not called after the empty check and before queue.wait() queue.wait(queue.isEmpty() ? properties.roundDuration() : Math.max(1, queue.peek().sendingTime - System.currentTimeMillis())); } } catch (InterruptedException e) { if (isRunning()) logger.error("Unexpected interrupt.", e); } } private void sendTransaction(Transaction transaction) { Tangle.TransactionLog transactionLog = node.ict.getTangle().findTransactionLog(transaction); if (Math.abs(transaction.issuanceTimestamp - System.currentTimeMillis()) > Constants.TIMESTAMP_DIFFERENCE_TOLERANCE_IN_MILLIS * 0.9) return; for (Neighbor nb : node.neighbors) if (transactionLog == null || !transactionLog.senders.contains(nb)) sendTransactionToNeighbor(nb, transaction); } private void sendTransactionToNeighbor(Neighbor nb, Transaction transaction) { try { DatagramPacket packet = transaction.toDatagramPacket(transactionsToRequest.isEmpty() ? Trytes.NULL_HASH : transactionsToRequest.poll()); packet.setSocketAddress(nb.getSocketAddress()); node.socket.send(packet); } catch (Exception e) { if (isRunning()) logger.error("Failed to send transaction to neighbor.", e); } } @Override public void onTerminate() { synchronized (queue) { queue.notify(); } } public void queue(Transaction transaction) { long forwardDelay = properties.minForwardDelay() + ThreadLocalRandom.current().nextLong(properties.maxForwardDelay() - properties.minForwardDelay()); queue.add(new SendingTask(System.currentTimeMillis() + forwardDelay, transaction)); synchronized (queue) { queue.notify(); } } @Override public void updateProperties(FinalProperties properties) { this.properties = properties; synchronized (queue) { // notify queue to stop wait() and enforce new round duration queue.notify(); } } @Override public void request(String requestedHash) { transactionsToRequest.add(requestedHash); } private static class SendingTask { private final long sendingTime; private final Transaction transaction; private SendingTask(long sendingTime, Transaction transaction) { this.sendingTime = sendingTime; this.transaction = transaction; } } public int queueSize() { return queue.size(); } private static class SendingTaskQueue extends PriorityBlockingQueue<SendingTask> { private SendingTaskQueue() { super(1, new Comparator<SendingTask>() { @Override public int compare(SendingTask task1, SendingTask task2) { return Long.compare(task1.sendingTime, task2.sendingTime); } }); } } }