Skip to content

Commit

Permalink
Fix: LS taking too long (iotaledger#1843)
Browse files Browse the repository at this point in the history
Changes code to use a more lightweight entrypoint generation.
No longer use arrival time to check if something is probably orphaned.
  • Loading branch information
Brord van Wierst authored May 5, 2020
1 parent 9ca0899 commit d6cdd03
Showing 1 changed file with 126 additions and 172 deletions.
298 changes: 126 additions & 172 deletions src/main/java/com/iota/iri/service/snapshot/impl/SnapshotServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.iota.iri.service.snapshot.impl;

import com.iota.iri.conf.IotaConfig;
import com.iota.iri.conf.SnapshotConfig;
import com.iota.iri.controllers.ApproveeViewModel;
import com.iota.iri.controllers.MilestoneViewModel;
Expand All @@ -22,16 +23,12 @@
import com.iota.iri.utils.log.ProgressLogger;
import com.iota.iri.utils.log.interval.IntervalProgressLogger;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

/**
* <p>
* Creates a service instance that allows us to access the business logic for {@link Snapshot}s.
Expand All @@ -46,36 +43,6 @@ public class SnapshotServiceImpl implements SnapshotService {
*/
private static final Logger log = LoggerFactory.getLogger(SnapshotServiceImpl.class);

/**
* <p>
* Holds a limit for the amount of milestones we go back in time when generating the solid entry points (to speed up
* the snapshot creation).
* </p>
* <p>
* Note: Since the snapshot creation is a "continuous" process where we build upon the information gathered during
* the creation of previous snapshots, we do not need to analyze all previous milestones but can rely on
* slowly gathering the missing information over time. While this might lead to a situation where the very
* first snapshots taken by a node might generate snapshot files that can not reliably be used by other nodes
* to sync it is still a reasonable trade-off to reduce the load on the nodes. We just assume that anybody who
* wants to share his snapshots with the community as a way to bootstrap new nodes will run his snapshot
* enabled node for a few hours before sharing his files (this is a problem in very rare edge cases when
* having back-referencing transactions anyway).
* </p>
*/
private static final int OUTER_SHELL_SIZE = 100;

/**
* <p>
* Maximum age in milestones since creation of solid entry points.
* </p>
* <p>
* Since it is possible to artificially keep old solid entry points alive by periodically attaching new transactions
* to them, we limit the life time of solid entry points and ignore them whenever they become too old. This is a
* measure against a potential attack vector where somebody might try to blow up the meta data of local snapshots.
* </p>
*/
private static final int SOLID_ENTRY_POINT_LIFETIME = 1000;

/**
* Holds the tangle object which acts as a database interface.
*/
Expand All @@ -89,15 +56,20 @@ public class SnapshotServiceImpl implements SnapshotService {
/**
* Holds the config with important snapshot specific settings.
*/
private final SnapshotConfig config;
private final IotaConfig config;

/**
* Minimum depth for generating solid entrypoints due to coordinator allowing 15 MS back attachment
*/
private static final int MIN_LS_DEPTH_MAINNET = 15 + 1;

/**
* Implements the snapshot service. See interface for more information.
* @param tangle acts as a database interface.
* @param snapshotProvider gives us access to the relevant snapshots.
* @param config configuration with snapshot specific settings.
*/
public SnapshotServiceImpl(Tangle tangle, SnapshotProvider snapshotProvider, SnapshotConfig config) {
public SnapshotServiceImpl(Tangle tangle, SnapshotProvider snapshotProvider, IotaConfig config) {
this.tangle = tangle;
this.snapshotProvider = snapshotProvider;
this.config = config;
Expand Down Expand Up @@ -278,6 +250,7 @@ public Snapshot generateSnapshot(MilestoneSolidifier milestoneSolidifier, Milest
public Map<Hash, Integer> generateSolidEntryPoints(MilestoneViewModel targetMilestone) throws SnapshotException {
Map<Hash, Integer> solidEntryPoints = new HashMap<>();
solidEntryPoints.put(Hash.NULL_HASH, targetMilestone.index());
solidEntryPoints.put(targetMilestone.getHash(), targetMilestone.index());

processOldSolidEntryPoints(tangle, snapshotProvider, targetMilestone, solidEntryPoints);
processNewSolidEntryPoints(tangle, snapshotProvider, targetMilestone, solidEntryPoints);
Expand Down Expand Up @@ -523,100 +496,9 @@ private void persistLocalSnapshot(SnapshotProvider snapshotProvider, Snapshot ne

snapshotProvider.getInitialSnapshot().update(newSnapshot);
}

/**
* <p>
* This method determines if a transaction is orphaned when none of its approvers is confirmed by a milestone.
* </p>
* <p>
* Since there is no hard definition for when a transaction can be considered to be orphaned, we define orphaned in
* relation to a referenceTransaction. If the transaction or any of its direct or indirect approvers saw a
* transaction being attached to it, that arrived after our reference transaction, we consider it "not orphaned".
* </p>
* <p>
* Since we currently use milestones as reference transactions that are sufficiently old, this definition in fact is
* a relatively safe way to determine if a subtangle "above" a transaction got orphaned.
* </p>
*
* @param tangle Tangle object which acts as a database interface
* @param transaction transaction that shall be checked
* @param referenceTransaction transaction that acts as a judge to the other transaction
* @param processedTransactions transactions that were visited already while trying to determine the orphaned status
* @return true if the transaction got orphaned and false otherwise
* @throws SnapshotException if anything goes wrong while determining the orphaned status
*/
private boolean isProbablyOrphaned(Tangle tangle, TransactionViewModel transaction,
TransactionViewModel referenceTransaction, Set<Hash> processedTransactions) throws SnapshotException {

AtomicBoolean nonOrphanedTransactionFound = new AtomicBoolean(false);
try {
DAGHelper.get(tangle).traverseApprovers(
transaction.getHash(),
currentTransaction -> !nonOrphanedTransactionFound.get(),
currentTransaction -> {
if (currentTransaction.getArrivalTime() / 1000L > referenceTransaction.getTimestamp()) {
nonOrphanedTransactionFound.set(true);
}
},
processedTransactions
);
} catch (TraversalException e) {
throw new SnapshotException("failed to determine orphaned status of " + transaction, e);
}

return !nonOrphanedTransactionFound.get();
}

/**
* <p>
* We determine whether future milestones will approve {@param transactionHash}. This should aid in determining
* solid entry points.
* </p>
* <p>
* To check if the transaction has non-orphaned approvers we first check if any of its approvers got confirmed by a
* future milestone, since this is very cheap. If none of them got confirmed by another milestone we do the more
* expensive check from {@link #isProbablyOrphaned(Tangle, TransactionViewModel, TransactionViewModel, Set)}.
* </p>
* <p>
* Since solid entry points have a limited life time and to prevent potential problems due to temporary errors in
* the database, we assume that the checked transaction is not orphaned if any error occurs while determining its
* status, thus adding solid entry points. This is a storage <=> reliability trade off, since the only bad effect of
* having too many solid entry points) is a bigger snapshot file.
* </p>
*
* @param tangle Tangle object which acts as a database interface
* @param transactionHash hash of the transaction that shall be checked
* @param targetMilestone milestone that is used as an anchor for our checks
* @return true if the transaction is a solid entry point and false otherwise
*/
private boolean isNotOrphaned(Tangle tangle, Hash transactionHash, MilestoneViewModel targetMilestone) {
Set<TransactionViewModel> unconfirmedApprovers = new HashSet<>();

try {
for (Hash approverHash : ApproveeViewModel.load(tangle, transactionHash).getHashes()) {
TransactionViewModel approver = TransactionViewModel.fromHash(tangle, approverHash);

if (approver.snapshotIndex() > targetMilestone.index()) {
return true;
} else if (approver.snapshotIndex() == 0) {
unconfirmedApprovers.add(approver);
}
}

Set<Hash> processedTransactions = new HashSet<>();
TransactionViewModel milestoneTransaction = TransactionViewModel.fromHash(tangle, targetMilestone.getHash());
for (TransactionViewModel unconfirmedApprover : unconfirmedApprovers) {
if (!isProbablyOrphaned(tangle, unconfirmedApprover, milestoneTransaction, processedTransactions)) {
return true;
}
}
} catch (Exception e) {
log.error("failed to determine the solid entry point status for transaction " + transactionHash, e);

return true;
}

return false;

private boolean isAboveMinMilestone(int fromIndex, int toIndex) {
return toIndex - fromIndex <= getSepDepth();
}

/**
Expand Down Expand Up @@ -646,9 +528,7 @@ private void processOldSolidEntryPoints(Tangle tangle, SnapshotProvider snapshot
for (Map.Entry<Hash, Integer> solidPoint : orgSolidEntryPoints.entrySet()) {
Hash hash = solidPoint.getKey();
int milestoneIndex = solidPoint.getValue();
if (!Hash.NULL_HASH.equals(hash)
&& targetMilestone.index() - milestoneIndex <= SOLID_ENTRY_POINT_LIFETIME
&& isNotOrphaned(tangle, hash, targetMilestone)) {
if (!Hash.NULL_HASH.equals(hash) && isAboveMinMilestone(milestoneIndex, targetMilestone.index())) {
TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, hash);
addTailsToSolidEntryPoints(milestoneIndex, solidEntryPoints, tvm);
solidEntryPoints.put(hash, milestoneIndex);
Expand All @@ -669,63 +549,137 @@ && isNotOrphaned(tangle, hash, targetMilestone)) {
* This method retrieves the new solid entry points of the snapshot reference given by the target milestone.
* </p>
* <p>
* A transaction is considered a solid entry point if it is a bundle tail that can be traversed down from a
* non-orphaned transaction that was approved by a milestone that is above the last local snapshot. Or if it is a
* bundle tail of a non-orphaned transaction that was approved by a milestone that is above the last local snapshot.
* A transaction is considered a solid entry point if it is a
* bundle tail of a non-orphaned transaction that was approved by a milestone that is above the target milestone.
*
* It iterates over all unprocessed milestones and analyzes their directly and indirectly approved transactions.
* Every transaction is checked for being not orphaned and the appropriate SEP is added to {@param SolidEntryPoints}
* It iterates over all relevant unprocessed milestones and analyzes their directly and indirectly approved transactions.
* Every transaction is checked for being SEP and added to {@param SolidEntryPoints} when required
* </p>
*
*
* @param tangle Tangle object which acts as a database interface
* @param snapshotProvider data provider for the {@link Snapshot}s that are relevant for the node
* @param targetMilestone milestone that is used to generate the solid entry points
* @param solidEntryPoints map that is used to collect the solid entry points
* @throws SnapshotException if anything goes wrong while determining the solid entry points
* @see #isNotOrphaned(Tangle, Hash, MilestoneViewModel)
* @see #getSolidEntryPoints(int, ProgressLogger)
*/
private void processNewSolidEntryPoints(Tangle tangle, SnapshotProvider snapshotProvider,
MilestoneViewModel targetMilestone, Map<Hash, Integer> solidEntryPoints) throws SnapshotException {

ProgressLogger progressLogger = new IntervalProgressLogger(
"Taking local snapshot [generating solid entry points]", log);

try {
progressLogger.start(Math.min(targetMilestone.index() - snapshotProvider.getInitialSnapshot().getIndex(),
OUTER_SHELL_SIZE));

MilestoneViewModel nextMilestone = targetMilestone;
while (nextMilestone != null && nextMilestone.index() > snapshotProvider.getInitialSnapshot().getIndex() &&
progressLogger.getCurrentStep() < progressLogger.getStepCount()) {

MilestoneViewModel currentMilestone = nextMilestone;
DAGHelper.get(tangle).traverseApprovees(
currentMilestone.getHash(),
currentTransaction -> currentTransaction.snapshotIndex() >= currentMilestone.index(),
currentTransaction -> {
if (isNotOrphaned(tangle, currentTransaction.getHash(), targetMilestone)) {
addTailsToSolidEntryPoints(targetMilestone.index(), solidEntryPoints,
currentTransaction);
}
}
);

solidEntryPoints.put(currentMilestone.getHash(), targetMilestone.index());

nextMilestone = MilestoneViewModel.findClosestPrevMilestone(tangle, currentMilestone.index(),
snapshotProvider.getInitialSnapshot().getIndex());

progressLogger.progress();
}

solidEntryPoints.putAll(getSolidEntryPoints(targetMilestone.index(), progressLogger));
progressLogger.finish();
} catch (Exception e) {
progressLogger.abort(e);

throw new SnapshotException("could not generate the solid entry points for " + targetMilestone, e);
}
}

/**
* Generates entrypoints based on target index down to the maximum depth of the node
*
* @param targetIndex The milestone index we target to generate entrypoints until.
* @param progressLogger The logger we use to write progress of entrypoint generation
* @return a map of entrypoints or <code>null</code> when we were interrupted
* @throws Exception When we fail to get entry points due to errors generally caused by db interaction
*/
private Map<Hash, Integer> getSolidEntryPoints(int targetIndex, ProgressLogger progressLogger) throws Exception {
Map<Hash, Integer> solidEntryPoints = new HashMap<>();
solidEntryPoints.put(Hash.NULL_HASH, targetIndex);
log.info("Generating entrypoints for {}", targetIndex);

int sepDepth = getSepDepth();
// Co back a but below the milestone. Limited to maxDepth or genisis
int startIndex = Math.max(snapshotProvider.getInitialSnapshot().getIndex(), targetIndex - sepDepth
) + 1; // cant start at last snapshot now can we, could be 0!

progressLogger.start(startIndex);

// Iterate from a reasonable old milestone to the target index to check for solid entry points
for (int milestoneIndex = startIndex; milestoneIndex <= targetIndex; milestoneIndex++) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}

MilestoneViewModel milestone = MilestoneViewModel.get(tangle, milestoneIndex);
if (milestone == null) {
log.warn("Failed to find milestone {} during entry point analyzation", milestoneIndex);
return null;
}

List<Hash> approvees = getMilestoneApprovees(milestoneIndex, milestone);
for (Hash approvee : approvees) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}

if (isSolidEntryPoint(approvee, targetIndex)) {
// A solid entry point should only be a tail transaction, otherwise the whole bundle can't be reproduced with a snapshot file
TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, approvee);
addTailsToSolidEntryPoints(milestoneIndex, solidEntryPoints, tvm);
}
}
progressLogger.progress();
}

return solidEntryPoints;
}

/**
* Calculates minimum solid entrypoint depth based on network and maxDepth
*
* @return The amount of ms we go back under target snapshot for generation of solid entrypoints
*/
private int getSepDepth() {
return config.isTestnet() ? config.getMaxDepth() : Math.min(MIN_LS_DEPTH_MAINNET, config.getMaxDepth());
}

/**
* isSolidEntryPoint checks whether any direct approver of the given transaction was confirmed
* by a milestone which is above the target milestone.
*
* @param txHash The hash we check as an entrypoint
* @param targetIndex
* @return if the transaction is considered a solid entrypoint
* @throws Exception on db error
*/
private boolean isSolidEntryPoint(Hash txHash, int targetIndex) throws Exception {
ApproveeViewModel approvers = ApproveeViewModel.load(tangle, txHash);
if (approvers.getHashes().isEmpty()) {
return false;
}

for (Hash approver : approvers.getHashes()) {
TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, approver);
if (tvm != null && tvm.snapshotIndex() > targetIndex) {
// confirmed by a later milestone than targetIndex => solidEntryPoint
return true;
}
}

return false;
}

/**
* getMilestoneApprovees traverses a milestone and collects all tx that were
* confirmed by that milestone or higher
*
* @param milestoneIndex
* @param milestone
* @return
* @throws TraversalException
*/
private List<Hash> getMilestoneApprovees(int milestoneIndex, MilestoneViewModel milestone) throws TraversalException {
List<Hash> approvees = new LinkedList<>();
DAGHelper.get(tangle).traverseApprovees(milestone.getHash(),
currentTransaction -> currentTransaction.snapshotIndex() == milestoneIndex,
currentTransaction -> {
approvees.add(currentTransaction.getHash());
});
return approvees;
}

private void addTailsToSolidEntryPoints(int milestoneIndex, Map<Hash, Integer> solidEntryPoints,
TransactionViewModel currentTransaction) throws TraversalException {
Expand Down

0 comments on commit d6cdd03

Please sign in to comment.