-
Notifications
You must be signed in to change notification settings - Fork 78
[ETCM-213] Reload bloom after restart #742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4217e5
a6258da
1bdc6b9
116782f
524a463
519f4d6
54e75c1
e9833d1
e4029e3
cc0cd99
6642568
5d37321
6e7bb45
29ca388
ce8a83f
f46fd50
1667f33
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 |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package io.iohk.ethereum.db | ||
|
|
||
| import java.nio.file.Files | ||
|
|
||
| import akka.util.ByteString | ||
| import cats.effect.Resource | ||
| import cats.effect.concurrent.{Deferred, Ref} | ||
| import io.iohk.ethereum.db.dataSource.{DataSourceUpdateOptimized, RocksDbConfig, RocksDbDataSource} | ||
| import io.iohk.ethereum.db.storage.{EvmCodeStorage, Namespaces, NodeStorage} | ||
| import io.iohk.ethereum.{FlatSpecBase, ResourceFixtures} | ||
| import monix.eval.Task | ||
| import monix.reactive.{Consumer, Observable} | ||
| import org.scalatest.matchers.should.Matchers | ||
|
|
||
| import scala.util.Random | ||
|
|
||
| class RockDbIteratorSpec extends FlatSpecBase with ResourceFixtures with Matchers { | ||
| type Fixture = RocksDbDataSource | ||
|
|
||
| override def fixtureResource: Resource[Task, RocksDbDataSource] = RockDbIteratorSpec.buildRockDbResource() | ||
|
|
||
| def genRandomArray(): Array[Byte] = { | ||
| val arr = new Array[Byte](32) | ||
| Random.nextBytes(arr) | ||
| arr | ||
| } | ||
|
|
||
| def genRandomByteString(): ByteString = { | ||
| ByteString.fromArrayUnsafe(genRandomArray()) | ||
| } | ||
|
|
||
| def writeNValuesToDb(n: Int, db: RocksDbDataSource, namespace: IndexedSeq[Byte]): Task[Unit] = { | ||
| val iterable = (0 until n) | ||
| Observable.fromIterable(iterable).foreachL { _ => | ||
| db.update(Seq(DataSourceUpdateOptimized(namespace, Seq(), Seq((genRandomArray(), genRandomArray()))))) | ||
| } | ||
| } | ||
|
|
||
| it should "cancel ongoing iteration" in testCaseT { db => | ||
| val largeNum = 1000000 | ||
| val finishMark = 20000 | ||
| for { | ||
| counter <- Ref.of[Task, Int](0) | ||
| cancelMark <- Deferred[Task, Unit] | ||
| _ <- writeNValuesToDb(largeNum, db, Namespaces.NodeNamespace) | ||
| fib <- db | ||
| .iterate(Namespaces.NodeNamespace) | ||
| .map(_.right.get) | ||
| .consumeWith(Consumer.foreachEval[Task, (Array[Byte], Array[Byte])] { _ => | ||
| for { | ||
| cur <- counter.updateAndGet(i => i + 1) | ||
| _ <- if (cur == finishMark) cancelMark.complete(()) else Task.unit | ||
| } yield () | ||
| }) | ||
| .start | ||
| _ <- cancelMark.get | ||
| // take in mind this test also check if all underlying rocksdb resources has been cleaned as if cancel | ||
| // would not close underlying DbIterator, whole test would kill jvm due to rocksdb error at native level because | ||
| // iterators needs to be closed before closing db. | ||
| _ <- fib.cancel | ||
| finalCounter <- counter.get | ||
| } yield { | ||
| assert(finalCounter < largeNum) | ||
| } | ||
| } | ||
|
|
||
| it should "read all key values in db" in testCaseT { db => | ||
| val largeNum = 100000 | ||
| for { | ||
| counter <- Ref.of[Task, Int](0) | ||
| _ <- writeNValuesToDb(largeNum, db, Namespaces.NodeNamespace) | ||
| _ <- db | ||
| .iterate(Namespaces.NodeNamespace) | ||
| .map(_.right.get) | ||
| .consumeWith(Consumer.foreachEval[Task, (Array[Byte], Array[Byte])] { _ => | ||
| counter.update(current => current + 1) | ||
| }) | ||
| finalCounter <- counter.get | ||
| } yield { | ||
| assert(finalCounter == largeNum) | ||
| } | ||
| } | ||
|
|
||
| it should "iterate over keys and values from different namespaces" in testCaseT { db => | ||
| val codeStorage = new EvmCodeStorage(db) | ||
| val codeKeyValues = (1 to 10).map(i => (ByteString(i.toByte), ByteString(i.toByte))).toList | ||
|
|
||
| val nodeStorage = new NodeStorage(db) | ||
| val nodeKeyValues = (20 to 30).map(i => (ByteString(i.toByte), ByteString(i.toByte).toArray)).toList | ||
|
|
||
| for { | ||
| _ <- Task(codeStorage.update(Seq(), codeKeyValues).commit()) | ||
| _ <- Task(nodeStorage.update(Seq(), nodeKeyValues)) | ||
| result <- Task.parZip2( | ||
| codeStorage.storageContent.map(_.right.get).map(_._1).toListL, | ||
| nodeStorage.storageContent.map(_.right.get).map(_._1).toListL | ||
| ) | ||
| (codeResult, nodeResult) = result | ||
| } yield { | ||
| codeResult shouldEqual codeKeyValues.map(_._1) | ||
| nodeResult shouldEqual nodeKeyValues.map(_._1) | ||
| } | ||
| } | ||
|
|
||
| it should "iterate over keys and values " in testCaseT { db => | ||
| val keyValues = (1 to 100).map(i => (ByteString(i.toByte), ByteString(i.toByte))).toList | ||
| for { | ||
| _ <- Task( | ||
| db.update( | ||
| Seq( | ||
| DataSourceUpdateOptimized(Namespaces.NodeNamespace, Seq(), keyValues.map(e => (e._1.toArray, e._2.toArray))) | ||
| ) | ||
| ) | ||
| ) | ||
| elems <- db.iterate(Namespaces.NodeNamespace).map(_.right.get).toListL | ||
| } yield { | ||
| val deserialized = elems.map { case (bytes, bytes1) => (ByteString(bytes), ByteString(bytes1)) } | ||
| assert(elems.size == keyValues.size) | ||
| assert(keyValues == deserialized) | ||
| } | ||
| } | ||
|
|
||
| it should "return empty list when iterating empty db" in testCaseT { db => | ||
| for { | ||
| elems <- db.iterate().toListL | ||
| } yield { | ||
| assert(elems.isEmpty) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| object RockDbIteratorSpec { | ||
| def getRockDbTestConfig(dbPath: String) = { | ||
| new RocksDbConfig { | ||
| override val createIfMissing: Boolean = true | ||
| override val paranoidChecks: Boolean = false | ||
| override val path: String = dbPath | ||
| override val maxThreads: Int = 1 | ||
| override val maxOpenFiles: Int = 32 | ||
| override val verifyChecksums: Boolean = false | ||
| override val levelCompaction: Boolean = true | ||
| override val blockSize: Long = 16384 | ||
| override val blockCacheSize: Long = 33554432 | ||
| } | ||
| } | ||
|
|
||
| def buildRockDbResource(): Resource[Task, RocksDbDataSource] = { | ||
| Resource.make { | ||
| Task { | ||
| val tempDir = Files.createTempDirectory("temp-iter-dir") | ||
| RocksDbDataSource(getRockDbTestConfig(tempDir.toAbsolutePath.toString), Namespaces.nsSeq) | ||
| } | ||
| }(db => Task(db.destroy())) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package io.iohk.ethereum.blockchain.sync | ||
|
|
||
| import com.google.common.hash.{BloomFilter, Funnel} | ||
| import io.iohk.ethereum.blockchain.sync.LoadableBloomFilter.BloomFilterLoadingResult | ||
| import io.iohk.ethereum.db.dataSource.RocksDbDataSource.IterationError | ||
| import monix.eval.Task | ||
| import monix.reactive.{Consumer, Observable} | ||
|
|
||
| class LoadableBloomFilter[A](bloomFilter: BloomFilter[A], source: Observable[Either[IterationError, A]]) { | ||
| val loadFromSource: Task[BloomFilterLoadingResult] = { | ||
|
Contributor
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 have mixed feelings about this setup TBH. If underlying bloom filter can be used only once loading completes - let's make a function which takes a source and returns task with ready-to-use bloom filter.
Contributor
Author
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. for now we are using underlying bloom filter that way (first load then use), as it is simplest possible scenario and loading time for etc is relatively shot i.e 2-3 min. As underlying bloom filter is thread safe, we could probably start loading filter from storage and at the same time start state sync, but i would do this as separate task. (It could be especially useful if we were to support ETH when loading nodes would take probably more that 10min which is probably not acceptable) That why i would leave it as it is. |
||
| source | ||
| .consumeWith(Consumer.foldLeftTask(BloomFilterLoadingResult()) { (s, e) => | ||
| e match { | ||
| case Left(value) => Task.now(s.copy(error = Some(value))) | ||
| case Right(value) => Task(bloomFilter.put(value)).map(_ => s.copy(writtenElements = s.writtenElements + 1)) | ||
| } | ||
| }) | ||
| .memoizeOnSuccess | ||
| } | ||
|
|
||
| def put(elem: A): Boolean = bloomFilter.put(elem) | ||
kapke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def mightContain(elem: A): Boolean = bloomFilter.mightContain(elem) | ||
|
|
||
| def approximateElementCount: Long = bloomFilter.approximateElementCount() | ||
| } | ||
|
|
||
| object LoadableBloomFilter { | ||
| def apply[A](expectedSize: Int, loadingSource: Observable[Either[IterationError, A]])(implicit | ||
| f: Funnel[A] | ||
| ): LoadableBloomFilter[A] = { | ||
| new LoadableBloomFilter[A](BloomFilter.create[A](f, expectedSize), loadingSource) | ||
| } | ||
|
|
||
| case class BloomFilterLoadingResult(writtenElements: Long, error: Option[IterationError]) | ||
| object BloomFilterLoadingResult { | ||
| def apply(): BloomFilterLoadingResult = new BloomFilterLoadingResult(0, None) | ||
|
|
||
| def apply(ex: Throwable): BloomFilterLoadingResult = new BloomFilterLoadingResult(0, Some(IterationError(ex))) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.