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

com.wavesplatform.network.MicroBlockSynchronizer.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.network

import com.google.common.cache.{Cache, CacheBuilder}
import com.wavesplatform.block.Block.BlockId
import com.wavesplatform.block.MicroBlock
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.metrics.BlockStats
import com.wavesplatform.settings.SynchronizationSettings.MicroblockSynchronizerSettings
import com.wavesplatform.utils.ScorexLogging
import io.netty.channel.*
import monix.eval.{Coeval, Task}
import monix.execution.CancelableFuture
import monix.execution.schedulers.SchedulerService
import monix.reactive.Observable

import java.util.concurrent.TimeUnit
import scala.collection.mutable.Set as MSet
import scala.concurrent.duration.FiniteDuration

object MicroBlockSynchronizer extends ScorexLogging {

  def apply(
      settings: MicroblockSynchronizerSettings,
      isLightMode: Boolean,
      peerDatabase: PeerDatabase,
      lastBlockIdEvents: Observable[ByteStr],
      microblockInvs: ChannelObservable[MicroBlockInv],
      microblockResponses: ChannelObservable[MicroBlockResponse],
      microblockSnapshots: ChannelObservable[MicroBlockSnapshotResponse],
      scheduler: SchedulerService
  ): (Observable[(Channel, MicroblockData, Option[(Channel, MicroBlockSnapshotResponse)])], Coeval[CacheSizes]) = {

    implicit val schdlr: SchedulerService = scheduler

    val microBlockOwners     = cache[MicroBlockSignature, MSet[Channel]](settings.invCacheTimeout)
    val nextInvs             = cache[MicroBlockSignature, MicroBlockInv](settings.invCacheTimeout)
    val awaiting             = cache[MicroBlockSignature, MicroBlockInv](settings.invCacheTimeout)
    val waitingForSnapshot   = cache[MicroBlockSignature, (Channel, MicroblockData)](settings.invCacheTimeout)
    val successfullyReceived = cache[MicroBlockSignature, Object](settings.processedMicroBlocksCacheTimeout)
    val receivedSnapshots    = cache[MicroBlockSignature, Object](settings.processedMicroBlocksCacheTimeout)

    val lastBlockId = lastObserved(lastBlockIdEvents)

    def owners(totalRef: BlockId): Set[Channel] = Option(microBlockOwners.getIfPresent(totalRef)).getOrElse(MSet.empty).toSet

    def alreadyRequested(totalRef: MicroBlockSignature): Boolean = Option(awaiting.getIfPresent(totalRef)).isDefined

    val cacheSizesReporter = Coeval.eval {
      CacheSizes(microBlockOwners.size(), nextInvs.size(), awaiting.size(), successfullyReceived.size())
    }

    def requestData[A](
        totalBlockId: MicroBlockSignature,
        linkedData: A,
        awaitingCache: Cache[MicroBlockSignature, A],
        receivedDataCache: Cache[MicroBlockSignature, Object],
        requestMessageF: MicroBlockSignature => Message,
        dataType: String
    ): CancelableFuture[Unit] = {

      def randomOwner(exclude: Set[Channel]) = random(owners(totalBlockId) -- exclude)

      def task(attemptsAllowed: Int, exclude: Set[Channel]): Task[Unit] = Task.defer {
        if (attemptsAllowed <= 0) {
          log.trace(s"No more attempts left to download $dataType $totalBlockId")
          Task.unit
        } else if (Option(receivedDataCache.getIfPresent(totalBlockId)).isDefined) {
          Task.unit
        } else
          randomOwner(exclude) match {
            case None =>
              log.trace(s"No owners found for $dataType $totalBlockId")
              Task.unit
            case Some(channel) =>
              if (channel.isOpen) {
                log.trace(s"${id(channel)} Requesting $dataType $totalBlockId")
                val request = requestMessageF(totalBlockId)
                awaitingCache.put(totalBlockId, linkedData)
                channel.writeAndFlush(request)
                task(attemptsAllowed - 1, exclude + channel).delayExecution(settings.waitResponseTimeout)
              } else task(attemptsAllowed, exclude + channel)
          }
      }

      task(MicroBlockDownloadAttempts, Set.empty).runAsyncLogErr
    }

    def tryDownloadNext(prevBlockId: ByteStr): Unit = Option(nextInvs.getIfPresent(prevBlockId)).foreach { inv =>
      requestData(inv.totalBlockId, inv, awaiting, successfullyReceived, MicroBlockRequest.apply, "microblock")
    }

    lastBlockIdEvents
      .mapEval { f =>
        log.trace(s"Last block id is now $f")
        Task(tryDownloadNext(f))
      }
      .executeOn(scheduler)
      .logErr
      .subscribe()

    microblockInvs
      .mapEval { case (ch, mbInv @ MicroBlockInv(_, totalBlockId, reference, _)) =>
        Task.evalAsync {
          val sig =
            try mbInv.signaturesValid()
            catch {
              case t: Throwable =>
                log.error(s"Error validating signature", t)
                throw t
            }
          sig match {
            case Left(err) =>
              peerDatabase.blacklistAndClose(ch, err.toString)
            case Right(_) =>
              microBlockOwners.get(totalBlockId, () => MSet.empty) += ch
              nextInvs.get(
                reference,
                { () =>
                  BlockStats.inv(mbInv, ch)
                  mbInv
                }
              )
              lastBlockId() match {
                case Some(`reference`) if !alreadyRequested(totalBlockId) => tryDownloadNext(reference)
                case _ => // either the microblock has already been requested or it does no reference the last block
              }
          }
        }.logErr
      }
      .executeOn(scheduler)
      .logErr
      .subscribe()

    val mbResponsesObservable = microblockResponses.observeOn(scheduler).flatMap { case (ch, MicroBlockResponse(mb, totalRef)) =>
      successfullyReceived.put(totalRef, dummy)
      BlockStats.received(mb, ch, totalRef)
      Option(awaiting.getIfPresent(totalRef)) match {
        case None =>
          log.trace(s"${id(ch)} Received unexpected ${mb.stringRepr(totalRef)}")
          Observable.empty
        case Some(mi) =>
          log.trace(s"${id(ch)} Received ${mb.stringRepr(totalRef)}, as expected")
          awaiting.invalidate(totalRef)
          Observable((ch, MicroblockData(Option(mi), mb, Coeval.evalOnce(owners(totalRef)))))
      }
    }

    val observable = if (isLightMode) {
      mbResponsesObservable
        .mapEval {
          case (ch, mbd @ MicroblockData(Some(mbInv), _, _)) =>
            Task.evalAsync {
              requestData(mbInv.totalBlockId, ch -> mbd, waitingForSnapshot, receivedSnapshots, MicroSnapshotRequest.apply, "microblock snapshot")
            }
          case _ => Task.unit
        }
        .executeOn(scheduler)
        .logErr
        .subscribe()

      microblockSnapshots.observeOn(scheduler).flatMap { case (ch, snapshot) =>
        BlockStats.received(ch, snapshot.totalBlockId)
        Option(receivedSnapshots.getIfPresent(snapshot.totalBlockId)) match {
          case Some(_) =>
            waitingForSnapshot.invalidate(snapshot.totalBlockId)
            log.trace(s"${id(ch)} Received snapshot for processed microblock ${snapshot.totalBlockId}, ignoring")
            Observable.empty
          case None =>
            Option(waitingForSnapshot.getIfPresent(snapshot.totalBlockId)) match {
              case Some((mbdCh, mbd)) =>
                receivedSnapshots.put(snapshot.totalBlockId, dummy)
                waitingForSnapshot.invalidate(snapshot.totalBlockId)
                log.trace(s"${id(ch)} Received microblock snapshot ${snapshot.totalBlockId}, as expected")
                Observable((mbdCh, mbd, Some(ch -> snapshot)))
              case None =>
                log.trace(s"${id(ch)} Received unexpected snapshot ${snapshot.totalBlockId}")
                Observable.empty
            }
        }
      }
    } else {
      mbResponsesObservable.map { case (ch, mbData) =>
        (ch, mbData, None)
      }
    }

    (observable, cacheSizesReporter)
  }

  case class MicroblockData(invOpt: Option[MicroBlockInv], microBlock: MicroBlock, microblockOwners: Coeval[Set[Channel]])

  type MicroBlockSignature = ByteStr

  private val MicroBlockDownloadAttempts = 2

  def random[T](s: Set[T]): Option[T] =
    if (s.isEmpty) None
    else {
      val n = util.Random.nextInt(s.size)
      s.drop(n).headOption
    }

  def cache[K <: AnyRef, V <: AnyRef](timeout: FiniteDuration): Cache[K, V] =
    CacheBuilder
      .newBuilder()
      .expireAfterWrite(timeout.toMillis, TimeUnit.MILLISECONDS)
      .build[K, V]()

  case class CacheSizes(microBlockOwners: Long, nextInvs: Long, awaiting: Long, successfullyReceived: Long)

  private val dummy = new Object()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy