All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.myodov.unicherrygarden.cherrypicker.syncers.AbstractSyncer.scala Maven / Gradle / Ivy

Go to download

UniCherryGarden: CherryPicker – the subsystem that keeps track of Ethereum blockchain data, stores it in the DB storage and “cherry-picks” the data on the fly (selective currencies, selective addresses to watch)

The newest version!
package com.myodov.unicherrygarden.cherrypicker.syncers

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import com.myodov.unicherrygarden.AbstractEthereumNodeConnector.SingleBlockData
import com.myodov.unicherrygarden.api.DBStorage.Progress
import com.myodov.unicherrygarden.api.GardenMessages.{HeadSyncerMessage, TailSyncerMessage}
import com.myodov.unicherrygarden.api.types.SystemStatus
import com.myodov.unicherrygarden.api.{DBStorage, DBStorageAPI, GardenMessages, dlt}
import com.myodov.unicherrygarden.{AbstractEthereumNodeConnector, CherryGardenComponent, Web3ReadOperations}
import com.typesafe.scalalogging.LazyLogging
import scalikejdbc.DBSession

import scala.language.postfixOps
import scala.util.control.NonFatal

/** Superclass for [[HeadSyncer]] and [[TailSyncer]], containing the common logic.
 *
 * Generic class arguments:
 * 
    *
  • `M` – one of [[HeadSyncerMessage]], [[TailSyncerMessage]].
  • *
  • `S` – one of [[HeadSyncerState]], [[TailSyncerState]].
  • *
  • `IS` – one of [[IterateHeadSyncer]], [[IterateTailSyncer]].
  • *
* * `state` contains the overall state of the syncer. * Unfortunately, we cannot go fully Akka-way having the system state passed through the methods, FSM states * and behaviors. If we receive some state-changing message from outside (e.g. the latest state of Ethereum node * syncing process; or, for HeadSyncer, the message from TailSyncer), we need to alter the state immediately. * But the FSM may be in a 10-second delay after the latest block being processed, and after it a message * with the previous state will be posted by the timer. So alas, `state` has to be variable. */ abstract private class AbstractSyncer[ M <: GardenMessages.SyncerMessage, S <: AbstractSyncer.SyncerState, IS <: GardenMessages.IterateSyncer[M] with M ] (protected[this] val dbStorage: DBStorageAPI, protected[this] val ethereumConnector: AbstractEthereumNodeConnector with Web3ReadOperations, protected[this] val state: S) extends LazyLogging { /** Most important method doing some next iteration of a syncer; must be implemented. */ def iterate(): Behavior[M] /** Construct the specific implementation of `S` generic instance; must be implemented. */ val iterateMessage: IS /** Most basic sanity test for the DB data; * fails if we cannot even go further and must wait for the node to continue syncing. */ protected[this] def isNodeReachable(dbProgressData: DBStorage.Progress.ProgressData, nodeSyncingStatus: SystemStatus.Blockchain): Boolean = dbProgressData.blocks.to match { case None => // All data is available; but there is no blocks in the DB. This actually is fully okay true case Some(maxBlocksNum) => // Everything is fine if our latest stored block is not newer than the latest block available // to Ethereum node; // false/bad otherwise maxBlocksNum <= nodeSyncingStatus.syncingData.currentBlock } /** Validate the syncing progress data/Ethereum node syncing state; * and execute the `code` if they are valid, assuming it (maybe) returns some `Behavior`. * * `RES` is the expected return type from the function; may be `Option[Behavior]`, `[Behavior]` or something similar. * * @param onError the behavior generator that should be invoked in case of error; the result of invokation * will be returned * @param code the function that will be passed the `ProgressData` and `EthereumNodeStatus` objects. * It can rely upon the fact that `progress.overall.from` is not `None` and contains something. */ protected[this] def withValidatedProgressAndSyncingState[RES]( optProgress: Option[Progress.ProgressData], optNodeSyncingStatus: Option[SystemStatus.Blockchain], onError: () => RES ) ( code: (Progress.ProgressData, SystemStatus.Blockchain) => RES ) (implicit session: DBSession): RES = { (optProgress, optNodeSyncingStatus) match { case (None, _) => // we could not even get the DB progress – go to the next round logger.error("Some unexpected error when reading the overall progress from the DB") onError() case (_, None) => // we haven’t received the syncing state from the node logger.debug("No syncing status from Ethereum node is available yet, waiting") onError() case (Some(overallProgress: Progress.ProgressData), Some(nodeSyncStatus: SystemStatus.Blockchain)) if !isNodeReachable(overallProgress, nodeSyncStatus) => // Sanity test. Does the overall data sanity allows us to proceed? // In HeadSyncer, this is Reorg/rewind, phase 1/4: “is node reachable”. // In TailSyncer, this is just a sanity test. logger.error(s"Ethereum node is probably unavailable: $overallProgress, $nodeSyncStatus") onError() case (Some(overallProgress: Progress.ProgressData), Some(nodeSyncStatus: SystemStatus.Blockchain)) => // Both CherryPicker syncing progress and Ethereum node status are at least available; // but let’s validate them, and only then launch the code. if (!overallProgress.isConfigurationValid) { onError() } else { code(overallProgress, nodeSyncStatus) } } } def reiterate(): Behavior[M] = { logger.debug("FSM: reiterate") Behaviors.setup { context => logger.debug("Sending iterate message to self") context.self ! iterateMessage Behaviors.same } } def pauseThenReiterate(): Behavior[M] = { logger.debug("FSM: pauseThenReiterate") Behaviors.withTimers[M] { timers => timers.startSingleTimer( iterateMessage, CherryGardenComponent.BLOCK_ITERATION_PERIOD) Behaviors.same } } /** The pause-then-reiterate method that must be implemented in each syncer specifically. */ def pauseThenReiterateOnError(): Behavior[M] /** Perform the regular iteration for a specific block number: * read the block from the Ethereum connector, store it into the DB. * * @return whether syncing of the blocks succeeded. */ protected[this] def syncBlocks( blocksToSync: dlt.EthereumBlock.BlockNumberRange )(implicit session: DBSession): Boolean = { val trackedAddresses: Set[String] = dbStorage.trackedAddresses.getJustAddresses logger.debug(s"FSM: syncBlocks - blocks $blocksToSync with tracked addresses $trackedAddresses") // Were all of the blocks read and stored well? val successes: Seq[Boolean] = ethereumConnector.readBlocks(blocksToSync, trackedAddresses) match { case None => logger.error(s"Cannot read blocks $blocksToSync") Seq(false) case Some(blocks: Seq[SingleBlockData]) => blocks.map { case (block, transactions) => try { logger.debug(s"Reading block $block: txes $transactions") val thisBlockInDbOpt = dbStorage.blocks.getBlockByNumber(block.number) val prevBlockInDbOpt = dbStorage.blocks.getBlockByNumber(block.number - 1) logger.debug(s"Storing block: $block; " + s"block may be present as $thisBlockInDbOpt, " + s"parent may be present as $prevBlockInDbOpt") val addingBlockSuccess: Boolean = (thisBlockInDbOpt, prevBlockInDbOpt) match { case (None, None) => // This is the simplest case: this is probably the very first block in the DB logger.debug(s"Adding first block ${block.number}: " + s"neither it nor previous block exist in the DB") dbStorage.blocks.addBlock(block.withoutParentHash) true case (None, Some(prevBlockInDb)) if prevBlockInDb.hash == block.parentHash.get => // Another simplest case: second and further blocks in the DB. // Very new block, and its parent matches the existing one logger.debug(s"Adding new block ${block.number}; parent block ${block.number - 1} " + s"exists already with proper hash") dbStorage.blocks.addBlock(block) true case (Some(thisBlockInDb), _) if thisBlockInDb.hash == block.hash => logger.debug(s"Block ${block.number} exists already in the DB " + s"with the same hash ${block.hash}; " + "no need to readd the block itself") true case (Some(thisBlockInDb), _) if thisBlockInDb.hash != block.hash => logger.debug(s"Block ${block.number} exists already in the DB " + s"but with ${thisBlockInDb.hash} rather than ${block.hash}; " + "need to wipe some blocks maybe!") false case (None, Some(prevBlockInDb)) if prevBlockInDb.hash != block.parentHash.get => logger.debug(s"Adding new block ${block.number}: " + s"expecting parent block to be ${prevBlockInDb.hash} but it is ${block.parentHash.get}; " + "need to wipe some blocks maybe!") false case other => logger.debug(s"No idea what's up with $thisBlockInDbOpt and $prevBlockInDbOpt") false } // addingBlockSuccess if (!addingBlockSuccess) { // Bail out early false } else { logger.debug(s"Now trying to store the transactions: $transactions") for (tx <- transactions) { dbStorage.transactions.addTransaction(tx, block.hash) dbStorage.txLogs.addTxLogs(block.number, tx.txhash, tx.txLogs) } dbStorage.state.advanceProgress(block.number, trackedAddresses) true } } catch { case NonFatal(e) => logger.error(s"Unexpected error", e) false } } } // successes successes.forall(identity) } } object AbstractSyncer { trait SyncerState { @volatile var ethereumNodeStatus: Option[SystemStatus.Blockchain] } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy