nimbus-eth1/nimbus/sync/snap/worker/pivot/find_missing_nodes.nim

205 lines
7.2 KiB
Nim
Raw Normal View History

# Nimbus
# Copyright (c) 2021 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed
# except according to those terms.
## Find missing nodes for healing
## ==============================
##
## This module searches for missing nodes in the database (which means that
## nodes which link to missing ones must exist.)
##
## Algorithm
## ---------
##
## * Find dangling node links in the current account trie by trying *plan A*,
## and continuing with *plan B* only if *plan A* fails.
##
## A. Try to find nodes with envelopes that have no account in common with
## any range interval of the `processed` set of the hexary trie. This
## action will
##
## + either determine that there are no such envelopes implying that the
## accounts trie is complete (then stop here)
##
## + or result in envelopes related to nodes that are all allocated on the
## accounts trie (fail, use *plan B* below)
##
## + or result in some envelopes related to dangling nodes.
##
## B. Employ the `hexaryInspect()` trie perusal function in a limited mode
## for finding dangling (i.e. missing) sub-nodes below the allocated nodes.
##
## C. Remove empry intervals from the accounting ranges. This is a pure
## maintenance process that applies if A and B fail.
##
## Discussion
## ----------
##
## For *plan A*, the complement of ranges in the `processed` is determined
## and expressed as a list of node envelopes. As a consequence, the gaps
## beween the envelopes are either blind ranges that have no leaf nodes in
## the databse, or they are contained in the `processed` range. These gaps
## will be silently merged into the `processed` set of ranges.
##
## For *plan B*, a worst case scenario of a failing *plan B* must be solved
## by fetching and storing more nodes with other means before using this
## algorithm to find more missing nodes.
##
## Due to the potentially poor performance using `hexaryInspect()`.there is
## no general solution for *plan B* by recursively searching the whole hexary
## trie database for more dangling nodes.
##
{.push raises: [].}
import
std/sequtils,
chronicles,
chronos,
eth/common,
stew/interval_set,
"../../.."/[sync_desc, types],
"../.."/[constants, range_desc, worker_desc],
../db/[hexary_desc, hexary_envelope, hexary_error, hexary_inspect,
hexary_nearby]
logScope:
topics = "snap-find"
type
MissingNodesSpecs* = object
## Return type for `findMissingNodes()`
missing*: seq[NodeSpecs]
level*: uint8
visited*: uint64
emptyGaps*: NodeTagRangeSet
const
extraTraceMessages = false # or true
## Enabled additional logging noise
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
template logTxt(info: static[string]): static[string] =
"Find missing nodes " & info
template ignExceptionOops(info: static[string]; code: untyped) =
try:
code
except CatchableError as e:
trace logTxt "Ooops", `info`=info, name=($e.name), msg=(e.msg)
template noExceptionOops(info: static[string]; code: untyped) =
try:
code
except CatchableError as e:
raiseAssert "Inconveivable (" &
info & "): name=" & $e.name & " msg=" & e.msg
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc findMissingNodes*(
ranges: SnapRangeBatchRef;
rootKey: NodeKey;
getFn: HexaryGetFn;
planBLevelMax: uint8;
planBRetryMax: int;
planBRetrySleepMs: int;
): Future[MissingNodesSpecs]
{.async.} =
## Find some missing nodes in the hexary trie database.
var nodes: seq[NodeSpecs]
# Plan A, try complement of `processed`
noExceptionOops("compileMissingNodesList"):
if not ranges.processed.isEmpty:
# Get unallocated nodes to be fetched
let rc = ranges.processed.hexaryEnvelopeDecompose(rootKey, getFn)
if rc.isOk:
# Extract nodes from the list that do not exisit in the database
# and need to be fetched (and allocated.)
let missing = rc.value.filterIt(it.nodeKey.ByteArray32.getFn().len == 0)
if 0 < missing.len:
when extraTraceMessages:
trace logTxt "plan A", nNodes=nodes.len, nMissing=missing.len
return MissingNodesSpecs(missing: missing)
when extraTraceMessages:
trace logTxt "plan A not applicable", nNodes=nodes.len
# Plan B, carefully employ `hexaryInspect()`
var nRetryCount = 0
if 0 < nodes.len:
ignExceptionOops("compileMissingNodesList"):
let
paths = nodes.mapIt it.partialPath
suspend = if planBRetrySleepMs <= 0: 1.nanoseconds
else: planBRetrySleepMs.milliseconds
var
maxLevel = planBLevelMax
stats = getFn.hexaryInspectTrie(rootKey, paths,
stopAtLevel = maxLevel,
maxDangling = fetchRequestTrieNodesMax)
while stats.dangling.len == 0 and
nRetryCount < planBRetryMax and
not stats.resumeCtx.isNil:
await sleepAsync suspend
nRetryCount.inc
maxLevel = (120 * maxLevel + 99) div 100 # ~20% increase
trace logTxt "plan B retry", nRetryCount, maxLevel
stats = getFn.hexaryInspectTrie(rootKey,
resumeCtx = stats.resumeCtx,
stopAtLevel = maxLevel,
maxDangling = fetchRequestTrieNodesMax)
result = MissingNodesSpecs(
missing: stats.dangling,
level: stats.level,
visited: stats.count)
if 0 < result.missing.len:
when extraTraceMessages:
trace logTxt "plan B", nNodes=nodes.len, nDangling=result.missing.len,
level=result.level, nVisited=result.visited, nRetryCount
return
when extraTraceMessages:
trace logTxt "plan B not applicable", nNodes=nodes.len,
level=result.level, nVisited=result.visited, nRetryCount
# Plan C, clean up intervals
# Calculate `gaps` as the complement of the `processed` set of intervals
let gaps = NodeTagRangeSet.init()
discard gaps.merge(low(NodeTag),high(NodeTag))
for w in ranges.processed.increasing: discard gaps.reduce w
# Clean up empty gaps in the processed range
result.emptyGaps = NodeTagRangeSet.init()
for gap in gaps.increasing:
let rc = gap.minPt.hexaryNearbyRight(rootKey,getFn)
if rc.isOk:
# So there is a right end in the database and there is no leaf in
# the right open interval interval [gap.minPt,rc.value).
discard result.emptyGaps.merge(gap.minPt, rc.value)
elif rc.error == NearbyBeyondRange:
discard result.emptyGaps.merge(gap.minPt, high(NodeTag))
when extraTraceMessages:
trace logTxt "plan C", nGapFixes=result.emptyGaps.chunks,
nGapOpen=(ranges.processed.chunks - result.emptyGaps.chunks)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------