# 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 # ------------------------------------------------------------------------------