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

@@ -1,15 +1,16 @@
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.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.proto.CachesProto
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.lettuce.core.api.reactive.RedisReactiveCommands
import org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.math.min
@@ -18,9 +19,8 @@ import kotlin.math.min
* Cache blocks in Redis database
*/
class BlocksRedisCache(
private val redis: RedisReactiveCommands<String, String>,
private val chain: Chain,
private val objectMapper: ObjectMapper
private val redis: RedisReactiveCommands<String, ByteArray>,
private val chain: Chain
) : Reader<BlockId, BlockContainer> {
companion object {
@@ -34,13 +34,56 @@ class BlocksRedisCache(
override fun read(key: BlockId): Mono<BlockContainer> {
return redis.get(key(key))
.map { data ->
val block = objectMapper.readValue(data, BlockJson::class.java)
BlockContainer.from(block, objectMapper)
fromProto(data)
}.onErrorResume {
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> {
return Mono.just(id)
.flatMap {
@@ -59,20 +102,21 @@ class BlocksRedisCache(
}
return Mono.just(block)
.flatMap { block ->
val data = String(block.json!!)
//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
//still can be replaced in the blockchain
val age = Instant.now().epochSecond - block.timestamp!!.epochSecond
val ttl = min(age, TimeUnit.MINUTES.toSeconds(MAX_CACHE_TIME_MINUTES))
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 {
Mono.empty()
}
}
.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
.onErrorResume {

View File

@@ -2,20 +2,18 @@ package io.emeraldpay.dshackle.cache
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.EnvVariables
import io.emeraldpay.grpc.Chain
import io.lettuce.core.RedisClient
import io.lettuce.core.RedisURI
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.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.env.Environment
import org.springframework.stereotype.Repository
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.PostConstruct
import kotlin.collections.HashMap
@Repository
@@ -29,7 +27,7 @@ class CachesFactory(
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)
@PostConstruct
@@ -56,15 +54,15 @@ class CachesFactory(
if (ping != "PONG") {
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 {
val caches = Caches.newBuilder()
.setObjectMapper(objectMapper)
redis?.let { redis ->
caches.setBlockByHash(BlocksRedisCache(redis.reactive(), chain, objectMapper))
caches.setTxByHash(TxRedisCache(redis.reactive(), chain, objectMapper))
caches.setBlockByHash(BlocksRedisCache(redis.reactive(), chain))
caches.setTxByHash(TxRedisCache(redis.reactive(), chain))
}
return caches.build()
}

View File

@@ -1,14 +1,15 @@
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.BlockId
import io.emeraldpay.dshackle.data.TxContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader
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 org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import reactor.util.function.Tuples
@@ -20,9 +21,8 @@ import kotlin.math.min
* Cache transactions in Redis, up to 24 hours.
*/
class TxRedisCache(
private val redis: RedisReactiveCommands<String, String>,
private val chain: Chain,
private val objectMapper: ObjectMapper
private val redis: RedisReactiveCommands<String, ByteArray>,
private val chain: Chain
) : Reader<TxId, TxContainer> {
companion object {
@@ -35,14 +35,46 @@ class TxRedisCache(
override fun read(key: TxId): Mono<TxContainer> {
return redis.get(key(key))
.map { data ->
val json = data
val tx = objectMapper.readValue(json, TransactionJson::class.java)
TxContainer.from(tx, objectMapper)
fromProto(data)
}.onErrorResume {
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> {
return Mono.just(block)
.map { block ->
@@ -68,16 +100,18 @@ class TxRedisCache(
}
return Mono.just(Tuples.of(tx, block))
.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
//keep for hour, but block create 10 seconds ago cache for 10 seconds, as it
//still can be replaced in the blockchain
val age = Instant.now().epochSecond - it.t2.timestamp!!.epochSecond
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 {
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
.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;
}