problem: redis cached makes JSON parsing only to extract metadata, ineffective

solution: encode in protobuf with metadata included
This commit is contained in:
Igor Artamonov
2020-04-16 23:05:13 -04:00
parent 9cf1827fe1
commit 51b25ab55d
9 changed files with 229 additions and 51 deletions

View File

@@ -10,6 +10,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.70' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.70'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
} }
} }
@@ -25,6 +26,7 @@ plugins {
id 'org.springframework.boot' version '2.1.4.RELEASE' id 'org.springframework.boot' version '2.1.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE'
id 'com.palantir.git-version' version '0.12.2' id 'com.palantir.git-version' version '0.12.2'
id "com.google.protobuf" version "0.8.12"
} }
@@ -165,6 +167,15 @@ jar {
afterEvaluate { afterEvaluate {
distZip.dependsOn(jar) distZip.dependsOn(jar)
compileKotlin.dependsOn(generateVersion) compileKotlin.dependsOn(generateVersion)
generateProto.dependsOn(clean)
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
plugins {
}
generateProtoTasks {
}
} }
task generateVersion() { task generateVersion() {

View File

@@ -1,12 +1,12 @@
# Languages # Languages
groovyVersion=2.5.5 groovyVersion=2.5.5
kotlinVersion=1.3.11 kotlinVersion=1.3.11
protocVersion=3.7.1
# Main Libs # Main Libs
slf4jVersion=1.7.25 slf4jVersion=1.7.25
jacksonVersion=2.9.8 jacksonVersion=2.9.8
grpcVersion=1.20.0 grpcVersion=1.20.0
protocVersion=3.7.1
protobufVersion=3.7.1 protobufVersion=3.7.1
# Core # Core

View File

@@ -1,15 +1,16 @@
package io.emeraldpay.dshackle.cache package io.emeraldpay.dshackle.cache
import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.ByteString
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.proto.CachesProto
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.lettuce.core.api.reactive.RedisReactiveCommands import io.lettuce.core.api.reactive.RedisReactiveCommands
import org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Instant import java.time.Instant
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.min import kotlin.math.min
@@ -18,9 +19,8 @@ import kotlin.math.min
* Cache blocks in Redis database * Cache blocks in Redis database
*/ */
class BlocksRedisCache( class BlocksRedisCache(
private val redis: RedisReactiveCommands<String, String>, private val redis: RedisReactiveCommands<String, ByteArray>,
private val chain: Chain, private val chain: Chain
private val objectMapper: ObjectMapper
) : Reader<BlockId, BlockContainer> { ) : Reader<BlockId, BlockContainer> {
companion object { companion object {
@@ -34,13 +34,56 @@ class BlocksRedisCache(
override fun read(key: BlockId): Mono<BlockContainer> { override fun read(key: BlockId): Mono<BlockContainer> {
return redis.get(key(key)) return redis.get(key(key))
.map { data -> .map { data ->
val block = objectMapper.readValue(data, BlockJson::class.java) fromProto(data)
BlockContainer.from(block, objectMapper)
}.onErrorResume { }.onErrorResume {
Mono.empty() Mono.empty()
} }
} }
fun toProto(value: BlockContainer): ByteArray {
if (value.full) {
throw IllegalArgumentException("Full Block is not supposed to be cached")
}
val meta = CachesProto.BlockMeta.newBuilder()
.setHash(ByteString.copyFrom(value.hash.value))
.setHeight(value.height)
.setDifficulty(ByteString.copyFrom(value.difficulty.toByteArray()))
.setTimestamp(value.timestamp.toEpochMilli())
value.transactions.forEach {
meta.addTxHashes(ByteString.copyFrom(it.value))
}
return CachesProto.ValueContainer.newBuilder()
.setType(CachesProto.ValueContainer.ValueType.BLOCK)
.setValue(ByteString.copyFrom(value.json!!))
.setBlockMeta(meta)
.build()
.toByteArray()
}
fun fromProto(msg: ByteArray): BlockContainer {
val value = CachesProto.ValueContainer.parseFrom(msg)
if (value.type != CachesProto.ValueContainer.ValueType.BLOCK) {
throw IllegalArgumentException("Expect BLOCK value, receive ${value.type}")
}
if (!value.hasBlockMeta()) {
throw IllegalArgumentException("Container doesn't have Block Meta")
}
val meta = value.blockMeta
return BlockContainer(
meta.height,
BlockId(meta.hash.toByteArray()),
BigInteger(meta.difficulty.toByteArray()),
Instant.ofEpochMilli(meta.timestamp),
false,
value.value.toByteArray(),
meta.txHashesList.map {
TxId(it.toByteArray())
}
)
}
fun evict(id: BlockId): Mono<Void> { fun evict(id: BlockId): Mono<Void> {
return Mono.just(id) return Mono.just(id)
.flatMap { .flatMap {
@@ -59,20 +102,21 @@ class BlocksRedisCache(
} }
return Mono.just(block) return Mono.just(block)
.flatMap { block -> .flatMap { block ->
val data = String(block.json!!)
//default caching time is age of the block, i.e. block create hour ago //default caching time is age of the block, i.e. block create hour ago
//keep for hour, but block create 10 seconds ago cache for 10 seconds, as it //keep for hour, but block create 10 seconds ago cache for 10 seconds, as it
//still can be replaced in the blockchain //still can be replaced in the blockchain
val age = Instant.now().epochSecond - block.timestamp!!.epochSecond val age = Instant.now().epochSecond - block.timestamp!!.epochSecond
val ttl = min(age, TimeUnit.MINUTES.toSeconds(MAX_CACHE_TIME_MINUTES)) val ttl = min(age, TimeUnit.MINUTES.toSeconds(MAX_CACHE_TIME_MINUTES))
if (ttl > MIN_CACHE_TIME_SECONDS) { if (ttl > MIN_CACHE_TIME_SECONDS) {
redis.setex(key(block.hash), ttl, data) val key = key(block.hash)
val value = toProto(block)
redis.setex(key, ttl, value)
} else { } else {
Mono.empty() Mono.empty()
} }
} }
.doOnError { .doOnError {
log.warn("Failed to save to Redis: ${it.message}") log.warn("Failed to save Block to Redis: ${it.message}")
} }
//if failed to cache, just continue without it //if failed to cache, just continue without it
.onErrorResume { .onErrorResume {

View File

@@ -2,20 +2,18 @@ package io.emeraldpay.dshackle.cache
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.EnvVariables
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.RedisURI import io.lettuce.core.RedisURI
import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.StatefulRedisConnection
import io.lettuce.core.codec.ByteArrayCodec
import io.lettuce.core.codec.RedisCodec
import io.lettuce.core.codec.StringCodec
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.env.Environment
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import kotlin.collections.HashMap
@Repository @Repository
@@ -29,7 +27,7 @@ class CachesFactory(
private const val CONFIG_PREFIX = "cache.redis" private const val CONFIG_PREFIX = "cache.redis"
} }
private var redis: StatefulRedisConnection<String, String>? = null private var redis: StatefulRedisConnection<String, ByteArray>? = null
private val all = EnumMap<Chain, Caches>(io.emeraldpay.grpc.Chain::class.java) private val all = EnumMap<Chain, Caches>(io.emeraldpay.grpc.Chain::class.java)
@PostConstruct @PostConstruct
@@ -56,15 +54,15 @@ class CachesFactory(
if (ping != "PONG") { if (ping != "PONG") {
throw IllegalStateException("Redis connection is not configured. Response: $ping") throw IllegalStateException("Redis connection is not configured. Response: $ping")
} }
redis = client.connect() redis = client.connect(RedisCodec.of(StringCodec.ASCII, ByteArrayCodec.INSTANCE))
} }
private fun initCache(chain: Chain): Caches { private fun initCache(chain: Chain): Caches {
val caches = Caches.newBuilder() val caches = Caches.newBuilder()
.setObjectMapper(objectMapper) .setObjectMapper(objectMapper)
redis?.let { redis -> redis?.let { redis ->
caches.setBlockByHash(BlocksRedisCache(redis.reactive(), chain, objectMapper)) caches.setBlockByHash(BlocksRedisCache(redis.reactive(), chain))
caches.setTxByHash(TxRedisCache(redis.reactive(), chain, objectMapper)) caches.setTxByHash(TxRedisCache(redis.reactive(), chain))
} }
return caches.build() return caches.build()
} }

View File

@@ -1,14 +1,15 @@
package io.emeraldpay.dshackle.cache package io.emeraldpay.dshackle.cache
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxContainer import io.emeraldpay.dshackle.data.TxContainer
import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.TransactionJson import io.emeraldpay.dshackle.proto.CachesProto
import io.lettuce.core.api.reactive.RedisReactiveCommands import io.lettuce.core.api.reactive.RedisReactiveCommands
import org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.util.function.Tuples import reactor.util.function.Tuples
@@ -20,9 +21,8 @@ import kotlin.math.min
* Cache transactions in Redis, up to 24 hours. * Cache transactions in Redis, up to 24 hours.
*/ */
class TxRedisCache( class TxRedisCache(
private val redis: RedisReactiveCommands<String, String>, private val redis: RedisReactiveCommands<String, ByteArray>,
private val chain: Chain, private val chain: Chain
private val objectMapper: ObjectMapper
) : Reader<TxId, TxContainer> { ) : Reader<TxId, TxContainer> {
companion object { companion object {
@@ -35,14 +35,46 @@ class TxRedisCache(
override fun read(key: TxId): Mono<TxContainer> { override fun read(key: TxId): Mono<TxContainer> {
return redis.get(key(key)) return redis.get(key(key))
.map { data -> .map { data ->
val json = data fromProto(data)
val tx = objectMapper.readValue(json, TransactionJson::class.java)
TxContainer.from(tx, objectMapper)
}.onErrorResume { }.onErrorResume {
Mono.empty() Mono.empty()
} }
} }
fun toProto(value: TxContainer): ByteArray {
val meta = CachesProto.TxMeta.newBuilder()
.setHash(ByteString.copyFrom(value.hash.value))
.setHeight(value.height)
value.blockId?.value?.let {
meta.setBlockHash(ByteString.copyFrom(it))
}
return CachesProto.ValueContainer.newBuilder()
.setType(CachesProto.ValueContainer.ValueType.TX)
.setValue(ByteString.copyFrom(value.json!!))
.setTxMeta(meta)
.build()
.toByteArray()
}
fun fromProto(msg: ByteArray): TxContainer {
val value = CachesProto.ValueContainer.parseFrom(msg)
if (value.type != CachesProto.ValueContainer.ValueType.TX) {
throw IllegalArgumentException("Expect TX value, receive ${value.type}")
}
if (!value.hasTxMeta()) {
throw IllegalArgumentException("Container doesn't have Tx Meta")
}
val meta = value.txMeta
return TxContainer(
meta.height,
TxId(meta.hash.toByteArray()),
BlockId(meta.blockHash.toByteArray()),
value.value.toByteArray()
)
}
fun evict(block: BlockContainer): Mono<Void> { fun evict(block: BlockContainer): Mono<Void> {
return Mono.just(block) return Mono.just(block)
.map { block -> .map { block ->
@@ -68,16 +100,18 @@ class TxRedisCache(
} }
return Mono.just(Tuples.of(tx, block)) return Mono.just(Tuples.of(tx, block))
.flatMap { .flatMap {
val data = String(it.t1.json!!) val key = key(it.t1.hash)
val value = toProto(it.t1)
//default caching time is age of the block, i.e. block create hour ago //default caching time is age of the block, i.e. block create hour ago
//keep for hour, but block create 10 seconds ago cache for 10 seconds, as it //keep for hour, but block create 10 seconds ago cache for 10 seconds, as it
//still can be replaced in the blockchain //still can be replaced in the blockchain
val age = Instant.now().epochSecond - it.t2.timestamp!!.epochSecond val age = Instant.now().epochSecond - it.t2.timestamp!!.epochSecond
val ttl = min(age, TimeUnit.HOURS.toSeconds(MAX_CACHE_TIME_HOURS)) val ttl = min(age, TimeUnit.HOURS.toSeconds(MAX_CACHE_TIME_HOURS))
redis.setex(key(it.t1.hash), ttl, data) //store
redis.setex(key, ttl, value)
} }
.doOnError { .doOnError {
log.warn("Failed to save to Redis: ${it.message}") log.warn("Failed to save TX to Redis: ${it.message}", it)
} }
//if failed to cache, just continue without it //if failed to cache, just continue without it
.onErrorResume { .onErrorResume {

View File

@@ -0,0 +1,40 @@
syntax = "proto3";
package emerald.dshackle;
option java_package = "io.emeraldpay.dshackle.proto";
option java_outer_classname = "CachesProto";
message ValueContainer {
ValueType type = 1;
Compression compression = 2;
bytes value = 3;
oneof meta_type {
BlockMeta block_meta = 4;
TxMeta tx_meta = 5;
}
enum ValueType {
UNKNOWN = 0;
BLOCK = 1;
TX = 2;
}
enum Compression {
NONE = 0;
}
}
message BlockMeta {
uint64 height = 1;
bytes hash = 2;
bytes difficulty = 3;
uint64 timestamp = 4;
repeated bytes tx_hashes = 5;
}
message TxMeta {
uint64 height = 1;
bytes hash = 2;
bytes block_hash = 3;
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.cache
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.IntegrationTestingCommons import io.emeraldpay.dshackle.test.IntegrationTestingCommons
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
@@ -20,7 +21,8 @@ import java.time.temporal.ChronoUnit
@IgnoreIf({ IntegrationTestingCommons.isDisabled("redis") }) @IgnoreIf({ IntegrationTestingCommons.isDisabled("redis") })
class BlocksRedisCacheSpec extends Specification { class BlocksRedisCacheSpec extends Specification {
StatefulRedisConnection<String, String> redis StatefulRedisConnection<String, byte[]> redis
BlocksRedisCache cache
String hash1 = "0xd3f34def3c56ba4e701540d15edaff9acd2a1c968a7ff83b3300ab5dfd5f6aab" String hash1 = "0xd3f34def3c56ba4e701540d15edaff9acd2a1c968a7ff83b3300ab5dfd5f6aab"
String hash2 = "0x4aabdaff9acd2f30d15e00ab5dfd5f6c56ba4ea1c968a7ff8d3f34de70153b33" String hash2 = "0x4aabdaff9acd2f30d15e00ab5dfd5f6c56ba4ea1c968a7ff8d3f34de70153b33"
@@ -30,17 +32,43 @@ class BlocksRedisCacheSpec extends Specification {
ObjectMapper objectMapper = TestingCommons.objectMapper() ObjectMapper objectMapper = TestingCommons.objectMapper()
def setup() { def setup() {
RedisClient client = IntegrationTestingCommons.redis() redis = IntegrationTestingCommons.redisConnection()
StatefulRedisConnection<String, String> connection = client.connect(); redis.sync().flushdb()
connection.sync().flushdb() cache = new BlocksRedisCache(
redis = connection redis.reactive(), Chain.ETHEREUM
)
}
def "Decode encoded"() {
setup:
BlockContainer cont = new BlockContainer(
100,
BlockId.from(hash3),
BigInteger.valueOf(10515),
Instant.ofEpochSecond(10501050),
false,
"test".bytes,
[TxId.from(hash2), TxId.from(hash1)]
)
when:
def enc = cache.toProto(cont)
def dec = cache.fromProto(enc)
then:
dec.height == 100
dec.hash.toHex() == hash3
dec.difficulty.toString() == "10515"
dec.timestamp == Instant.ofEpochSecond(10501050)
dec.json == "test".bytes
dec.transactions.size() == 2
dec.transactions[0].toHex() == hash2
dec.transactions[1].toHex() == hash1
dec == cont
} }
def "Add and read"() { def "Add and read"() {
setup: setup:
def cache = new BlocksRedisCache(
redis.reactive(), Chain.ETHEREUM, TestingCommons.objectMapper()
)
def block = new BlockJson<TransactionRefJson>() def block = new BlockJson<TransactionRefJson>()
block.number = 100 block.number = 100
block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS) block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS)
@@ -59,9 +87,6 @@ class BlocksRedisCacheSpec extends Specification {
def "Evict existing block"() { def "Evict existing block"() {
setup: setup:
def cache = new BlocksRedisCache(
redis.reactive(), Chain.ETHEREUM, TestingCommons.objectMapper()
)
def block = new BlockJson<TransactionRefJson>() def block = new BlockJson<TransactionRefJson>()
block.number = 100 block.number = 100
block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS) block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS)
@@ -86,9 +111,6 @@ class BlocksRedisCacheSpec extends Specification {
def "Evict non-existing block"() { def "Evict non-existing block"() {
setup: setup:
def cache = new BlocksRedisCache(
redis.reactive(), Chain.ETHEREUM, TestingCommons.objectMapper()
)
def block = new BlockJson<TransactionRefJson>() def block = new BlockJson<TransactionRefJson>()
block.number = 100 block.number = 100
block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS) block.timestamp = Instant.now().minusSeconds(100).truncatedTo(ChronoUnit.SECONDS)

View File

@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.cache
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxContainer import io.emeraldpay.dshackle.data.TxContainer
import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.IntegrationTestingCommons import io.emeraldpay.dshackle.test.IntegrationTestingCommons
@@ -15,6 +16,9 @@ import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.StatefulRedisConnection
import io.lettuce.core.codec.ByteArrayCodec
import io.lettuce.core.codec.RedisCodec
import io.lettuce.core.codec.StringCodec
import spock.lang.IgnoreIf import spock.lang.IgnoreIf
import spock.lang.Specification import spock.lang.Specification
@@ -33,11 +37,29 @@ class TxRedisCacheSpec extends Specification {
def objectMapper = TestingCommons.objectMapper() def objectMapper = TestingCommons.objectMapper()
def setup() { def setup() {
RedisClient client = IntegrationTestingCommons.redis() StatefulRedisConnection<String, byte[]> redis = IntegrationTestingCommons.redisConnection()
StatefulRedisConnection<String, String> connection = client.connect(); redis.sync().flushdb()
connection.sync().flushdb() cache = new TxRedisCache(redis.reactive(), Chain.ETHEREUM)
StatefulRedisConnection<String, String> redis = connection }
cache = new TxRedisCache(redis.reactive(), Chain.ETHEREUM, TestingCommons.objectMapper())
def "Decode encoded"() {
setup:
TxContainer cont = new TxContainer(
2000,
TxId.from(hash1),
BlockId.from(hash2),
"test".bytes
)
when:
def enc = cache.toProto(cont)
def dec = cache.fromProto(enc)
then:
dec.height == 2000
dec.hash.toHex() == hash1
dec.blockId.toHex() == hash2
dec.json == "test".bytes
dec == cont
} }
def "Add and read"() { def "Add and read"() {

View File

@@ -1,6 +1,10 @@
package io.emeraldpay.dshackle.test package io.emeraldpay.dshackle.test
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.api.StatefulRedisConnection
import io.lettuce.core.codec.ByteArrayCodec
import io.lettuce.core.codec.RedisCodec
import io.lettuce.core.codec.StringCodec
class IntegrationTestingCommons { class IntegrationTestingCommons {
@@ -22,4 +26,7 @@ class IntegrationTestingCommons {
return RedisClient.create("redis://${host}:${port}") return RedisClient.create("redis://${host}:${port}")
} }
static StatefulRedisConnection<String, byte[]> redisConnection() {
return redis().connect(RedisCodec.of(StringCodec.ASCII, ByteArrayCodec.INSTANCE))
}
} }