-
Notifications
You must be signed in to change notification settings - Fork 370
[Fix] LS taking too long #1843
[Fix] LS taking too long #1843
Changes from 3 commits
687bac7
0f38f89
463f39b
f2833c4
9376c05
9318993
3de6bea
81577d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
|
@@ -18,16 +19,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. | ||
|
@@ -42,36 +39,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. | ||
*/ | ||
|
@@ -85,15 +52,15 @@ public class SnapshotServiceImpl implements SnapshotService { | |
/** | ||
* Holds the config with important snapshot specific settings. | ||
*/ | ||
private final SnapshotConfig config; | ||
private final IotaConfig config; | ||
|
||
/** | ||
* 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; | ||
|
@@ -507,100 +474,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 <= config.getMaxDepth(); | ||
} | ||
|
||
/** | ||
|
@@ -630,9 +506,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); | ||
|
@@ -653,63 +527,134 @@ && 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(); | ||
Map<Hash, Integer> entryPoints = getSolidEntryPoints(targetMilestone.index(), progressLogger); | ||
|
||
// Only is null when we are shutting down / Thread interrupted | ||
if (entryPoints == null) { | ||
throw new SnapshotException("No solid entrypoint generation due to thread interruption"); | ||
} | ||
|
||
|
||
solidEntryPoints.putAll(entryPoints); | ||
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); | ||
|
||
// Co back a but below the milestone. Limited to maxDepth or genisis | ||
int startIndex = Math.max(snapshotProvider.getInitialSnapshot().getIndex(), targetIndex - config.getMaxDepth() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of historical reasons max depth is configurable... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, this is the reason why you shouldn't have changed the interface There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This used to be limited to 100 hardcoded due to outer_shell_size, whatever that means. However that meant i had to wait for 100 milestones to test, and seemed just idiotic anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that would be dangerous on mainnet... |
||
) + 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.interrupted()) { | ||
return null; | ||
} | ||
|
||
MilestoneViewModel milestone = MilestoneViewModel.get(tangle, milestoneIndex); | ||
if (milestone == null) { | ||
log.warn("Failed to find milestone {} during entry point analyzation", milestoneIndex); | ||
GalRogozinski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
|
||
List<Hash> approvees = getMilestoneApprovees(milestoneIndex, milestone); | ||
for (Hash approvee : approvees) { | ||
if (Thread.interrupted()) { | ||
return null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. usually when one resets the interrupted status (you are implicitly doing it here) , one throws an The reason is that technically when you look at this method you are only aware of its scope and you are not aware of whether this thread will die later on. Imagine a case where the thread was interrupted but continues to work, and on the next check You can argue that in this specific context it will work.. it may break if the code changes but this will probably not happen. Even though this may be correct, I ask you: why do things the wrong way to begin with? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above |
||
} | ||
|
||
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; | ||
} | ||
|
||
/** | ||
* 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 { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
really?
I mean why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getMaxDepth