Browse Source

Refactored transaction importer, to separate signature validation from importing.

Importing has to be single threaded since it requires the database lock, but there's nothing to stop us from validating signatures on multiple threads, as no lock is required. So it makes sense to separate these two functions to allow for possible multi threaded signature validation in the future, to speed up the process.

Everything remains single threaded in this commit. It should be functionally the same as before, to reduce risk.
height-v3
CalDescent 2 years ago
parent
commit
ab0fc07ee9
  1. 80
      src/main/java/org/qortal/controller/TransactionImporter.java

80
src/main/java/org/qortal/controller/TransactionImporter.java

@ -20,6 +20,7 @@ import org.qortal.utils.NTP;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class TransactionImporter extends Thread { public class TransactionImporter extends Thread {
@ -63,7 +64,9 @@ public class TransactionImporter extends Thread {
Thread.sleep(1000L); Thread.sleep(1000L);
// Process incoming transactions queue // Process incoming transactions queue
processIncomingTransactionsQueue(); validateTransactionsInQueue();
importTransactionsInQueue();
// Clean up invalid incoming transactions list // Clean up invalid incoming transactions list
cleanupInvalidTransactionsList(NTP.getTime()); cleanupInvalidTransactionsList(NTP.getTime());
} }
@ -90,7 +93,24 @@ public class TransactionImporter extends Thread {
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature)); incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature));
} }
private void processIncomingTransactionsQueue() { /**
* Retrieve all pending unconfirmed transactions that have had their signatures validated.
* @return a list of TransactionData objects, with valid signatures.
*/
private List<TransactionData> getCachedSigValidTransactions() {
return this.incomingTransactions.entrySet().stream()
.filter(t -> Boolean.TRUE.equals(t.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
/**
* Validate the signatures of any transactions pending import, then update their
* entries in the queue to mark them as valid/invalid.
*
* No database lock is required.
*/
private void validateTransactionsInQueue() {
if (this.incomingTransactions.isEmpty()) { if (this.incomingTransactions.isEmpty()) {
// Nothing to do? // Nothing to do?
return; return;
@ -127,6 +147,8 @@ public class TransactionImporter extends Thread {
if (isLiteNode) { if (isLiteNode) {
// Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid // Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid
sigValidTransactions.add(transaction); sigValidTransactions.add(transaction);
// Add mark signature as valid if transaction still exists in import queue
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
continue; continue;
} }
@ -166,30 +188,42 @@ public class TransactionImporter extends Thread {
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size()); LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
} }
if (sigValidTransactions.isEmpty()) { } catch (DataException e) {
// Don't bother locking if there are no new transactions to process LOGGER.error("Repository issue while processing incoming transactions", e);
return; }
} }
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) { /**
// Prioritize syncing, and don't attempt to lock * Import any transactions in the queue that have valid signatures.
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted *
return; * A database lock is required.
} */
private void importTransactionsInQueue() {
List<TransactionData> sigValidTransactions = this.getCachedSigValidTransactions();
if (sigValidTransactions.isEmpty()) {
// Don't bother locking if there are no new transactions to process
return;
}
try { if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); // Prioritize syncing, and don't attempt to lock
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) { return;
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted }
LOGGER.debug("Too busy to process incoming transactions queue");
return; try {
} ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
} catch (InterruptedException e) { if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) {
LOGGER.debug("Interrupted when trying to acquire blockchain lock"); LOGGER.debug("Too busy to process incoming transactions queue");
return; return;
} }
} catch (InterruptedException e) {
LOGGER.debug("Interrupted when trying to acquire blockchain lock");
return;
}
LOGGER.debug("Processing incoming transactions queue (size {})...", sigValidTransactions.size());
LOGGER.debug("Processing incoming transactions queue (size {})...", sigValidTransactions.size()); try (final Repository repository = RepositoryManager.getRepository()) {
// Import transactions with valid signatures // Import transactions with valid signatures
try { try {
@ -203,8 +237,8 @@ public class TransactionImporter extends Thread {
return; return;
} }
Transaction transaction = sigValidTransactions.get(i); TransactionData transactionData = sigValidTransactions.get(i);
TransactionData transactionData = transaction.getTransactionData(); Transaction transaction = Transaction.fromData(repository, transactionData);
Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed(); Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed();

Loading…
Cancel
Save