solution: cache latest blocks in memory
This commit is contained in:
30
src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksMemCache.kt
vendored
Normal file
30
src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksMemCache.kt
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package io.emeraldpay.dshackle.cache
|
||||
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class BlocksMemCache(
|
||||
val maxSize: Int = 64
|
||||
) {
|
||||
|
||||
private val mapping = ConcurrentHashMap<BlockHash, BlockJson<TransactionId>>()
|
||||
private val queue = ConcurrentLinkedQueue<BlockHash>()
|
||||
|
||||
fun get(hash: BlockHash): Mono<BlockJson<TransactionId>> {
|
||||
return Mono.justOrEmpty(mapping[hash])
|
||||
}
|
||||
|
||||
fun add(block: BlockJson<TransactionId>) {
|
||||
mapping.put(block.hash, block)
|
||||
queue.add(block.hash)
|
||||
|
||||
while (queue.size > maxSize) {
|
||||
val old = queue.remove()
|
||||
mapping.remove(old)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.emeraldpay.dshackle.reader
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.Commands
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.retry.Repeat
|
||||
import java.time.Duration
|
||||
|
||||
class BlockApiReader(
|
||||
val upstream: Upstream
|
||||
): Reader<BlockHash, BlockJson<TransactionId>> {
|
||||
|
||||
override fun read(key: BlockHash): Mono<BlockJson<TransactionId>> {
|
||||
return Mono.just(key)
|
||||
.flatMap {
|
||||
upstream.getApi(Selector.empty).executeAndConvert(Commands.eth().getBlock(it))
|
||||
}.repeatWhenEmpty { n ->
|
||||
Repeat.times<Any>(3)
|
||||
.exponentialBackoff(Duration.ofMillis(100), Duration.ofMillis(500))
|
||||
.apply(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.emeraldpay.dshackle.reader
|
||||
|
||||
import io.emeraldpay.dshackle.cache.BlocksMemCache
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class BlockCacheReader(
|
||||
val cache: BlocksMemCache
|
||||
): Reader<BlockHash, BlockJson<TransactionId>> {
|
||||
|
||||
override fun read(key: BlockHash): Mono<BlockJson<TransactionId>> {
|
||||
return cache.get(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.emeraldpay.dshackle.reader
|
||||
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class CompoundReader<K, D>(
|
||||
private val readers: Collection<Reader<K, D>>
|
||||
): Reader<K, D> {
|
||||
|
||||
override fun read(key: K): Mono<D> {
|
||||
if (readers.isEmpty()) {
|
||||
return Mono.empty()
|
||||
}
|
||||
var result = readers.first().read(key)
|
||||
if (readers.size == 1) {
|
||||
return result
|
||||
}
|
||||
readers.stream().skip(1).forEach {
|
||||
result = result.switchIfEmpty(it.read(key))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
10
src/main/kotlin/io/emeraldpay/dshackle/reader/EmptyReader.kt
Normal file
10
src/main/kotlin/io/emeraldpay/dshackle/reader/EmptyReader.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package io.emeraldpay.dshackle.reader
|
||||
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class EmptyReader<K, D>: Reader<K, D> {
|
||||
|
||||
override fun read(key: K): Mono<D> {
|
||||
return Mono.empty()
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt
Normal file
9
src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package io.emeraldpay.dshackle.reader
|
||||
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface Reader<K, D> {
|
||||
|
||||
fun read(key: K): Mono<D>
|
||||
|
||||
}
|
||||
@@ -31,16 +31,16 @@ class NativeCall(
|
||||
return requestMono.flatMapMany(this::prepareCall)
|
||||
.map(this::setupCallParams)
|
||||
.parallel()
|
||||
.flatMap(this::executeOnRemote)
|
||||
.flatMap(this::fetch)
|
||||
.sequential()
|
||||
.map(this::buildResponse)
|
||||
.doOnError { e -> log.warn("Error during native call", e) }
|
||||
.onErrorResume(this::processException)
|
||||
}
|
||||
|
||||
fun setupCallParams(it: CallContext<Tuple2<String, String>>): CallContext<Tuple2<String, List<Any>>> {
|
||||
val params = extractParams(it.payload.t2)
|
||||
return it.withPayload(Tuples.of(it.payload.t1, params))
|
||||
fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
|
||||
val params = extractParams(it.payload.params)
|
||||
return it.withPayload(ParsedCallDetails(it.payload.method, params))
|
||||
}
|
||||
|
||||
fun buildResponse(it: CallContext<ByteArray>): BlockchainOuterClass.NativeCallReplyItem {
|
||||
@@ -65,46 +65,57 @@ class NativeCall(
|
||||
.toMono()
|
||||
}
|
||||
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest): Flux<CallContext<Tuple2<String, String>>> {
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest): Flux<CallContext<RawCallDetails>> {
|
||||
val chain = Chain.byId(request.chain.number)
|
||||
if (chain == Chain.UNSPECIFIED) {
|
||||
return Flux.error<CallContext<Tuple2<String, String>>>(CallFailure(0, Exception("Invalid chain id: ${request.chain.number}")))
|
||||
return Flux.error(CallFailure(0, Exception("Invalid chain id: ${request.chain.number}")))
|
||||
}
|
||||
val upstream = upstreams.getUpstream(chain)
|
||||
?: return Flux.error<CallContext<Tuple2<String, String>>>(CallFailure(0, Exception("Chain ${chain.id} is unavailable")))
|
||||
?: return Flux.error(CallFailure(0, Exception("Chain ${chain.id} is unavailable")))
|
||||
|
||||
return prepareCall(request, upstream)
|
||||
}
|
||||
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<Tuple2<String, String>>> {
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<RawCallDetails>> {
|
||||
val matcher = Selector.convertToMatcher(request.selector)
|
||||
val apis = upstream.getApis(matcher)
|
||||
return request.itemsList.toFlux().map {
|
||||
val method = it.method
|
||||
val params = it.payload.toStringUtf8()
|
||||
val callQuorum = upstream.targets?.getQuorumFor(method) ?: AlwaysQuorum()
|
||||
callQuorum.init(upstream.getHead())
|
||||
|
||||
CallContext(it.id, apis, callQuorum, Tuples.of(method, params))
|
||||
CallContext(it.id, upstream, matcher, callQuorum, RawCallDetails(method, params))
|
||||
}
|
||||
}
|
||||
|
||||
fun executeOnRemote(ctx: CallContext<Tuple2<String, List<Any>>>): Mono<CallContext<ByteArray>> {
|
||||
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
return fetchFromCache(ctx)
|
||||
.switchIfEmpty(
|
||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchFromCache(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
val cachingApi = ctx.upstream.cache
|
||||
return cachingApi.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { ctx.withPayload(it) }
|
||||
}
|
||||
|
||||
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
val p: Predicate<Any> = CallQuorum.untilResolved(ctx.callQuorum)
|
||||
val all = ctx.apis.toFlux().share()
|
||||
val all = ctx.getApis().toFlux().share()
|
||||
//execute on the first API immediately, and then make a delay between each call to not dos upstreams
|
||||
val immediate = Flux.from(all).take(1)
|
||||
val retries = Flux.from(all).delayElements(Duration.ofMillis(200))
|
||||
return Flux.concat(immediate, retries)
|
||||
.takeWhile(p)
|
||||
.flatMap { api ->
|
||||
api.execute(ctx.id, ctx.payload.t1, ctx.payload.t2).map { Tuples.of(it, api.upstream!!) }
|
||||
api.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { Tuples.of(it, api.upstream!!) }
|
||||
}
|
||||
.reduce(ctx.callQuorum, CallQuorum.asReducer())
|
||||
.filter { it.isResolved() }
|
||||
.map {
|
||||
val result = it.getResult()
|
||||
?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.t1}"))
|
||||
?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.method}"))
|
||||
ctx.withPayload(result)
|
||||
}
|
||||
.onErrorMap {
|
||||
@@ -113,7 +124,7 @@ class NativeCall(
|
||||
else CallFailure(ctx.id, it)
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.error<CallContext<ByteArray>>(CallFailure(ctx.id, Exception("No response or no available upstream for ${ctx.payload.t1}")))
|
||||
Mono.error<CallContext<ByteArray>>(CallFailure(ctx.id, Exception("No response or no available upstream for ${ctx.payload.method}")))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,11 +136,18 @@ class NativeCall(
|
||||
return req as List<Any>
|
||||
}
|
||||
|
||||
open class CallContext<T>(val id: Int, val apis: Iterator<EthereumApi>, val callQuorum: CallQuorum, val payload: T) {
|
||||
open class CallContext<T>(val id: Int, val upstream: AggregatedUpstream, val matcher: Selector.Matcher, val callQuorum: CallQuorum, val payload: T) {
|
||||
fun <X> withPayload(payload: X): CallContext<X> {
|
||||
return CallContext(id, apis, callQuorum, payload)
|
||||
return CallContext(id, upstream, matcher, callQuorum, payload)
|
||||
}
|
||||
|
||||
fun getApis(): Iterator<DirectEthereumApi> {
|
||||
return upstream.getApis(matcher)
|
||||
}
|
||||
}
|
||||
|
||||
open class CallFailure(val id: Int, val reason: Throwable): Exception("Failed to call $id: ${reason.message}")
|
||||
|
||||
class RawCallDetails(val method: String, val params: String)
|
||||
class ParsedCallDetails(val method: String, val params: List<Any>)
|
||||
}
|
||||
@@ -1,19 +1,40 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.cache.BlocksMemCache
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.BlockCacheReader
|
||||
import io.emeraldpay.dshackle.reader.CompoundReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.Disposable
|
||||
import reactor.core.publisher.Flux
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Predicate
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
abstract class AggregatedUpstream(
|
||||
val targets: CallMethods
|
||||
): Upstream {
|
||||
val targets: CallMethods,
|
||||
val objectMapper: ObjectMapper
|
||||
): Upstream, Lifecycle {
|
||||
|
||||
private val blocksCache = BlocksMemCache()
|
||||
private var cacheSubscription: Disposable? = null
|
||||
private val blockReader: Reader<BlockHash, BlockJson<TransactionId>> = CompoundReader(
|
||||
listOf(BlockCacheReader(blocksCache))
|
||||
)
|
||||
var cache: CachingEthereumApi = CachingEthereumApi.empty()
|
||||
private val reconfigLock = ReentrantLock()
|
||||
|
||||
abstract fun getAll(): List<Upstream>
|
||||
abstract fun addUpstream(upstream: Upstream)
|
||||
abstract fun getApis(matcher: Selector.Matcher): Iterator<EthereumApi>
|
||||
abstract fun getApis(matcher: Selector.Matcher): Iterator<DirectEthereumApi>
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
||||
@@ -61,4 +82,23 @@ abstract class AggregatedUpstream(
|
||||
return changed
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
cacheSubscription?.dispose()
|
||||
cacheSubscription = null
|
||||
}
|
||||
|
||||
fun onHeadUpdated(head: EthereumHead) {
|
||||
reconfigLock.withLock {
|
||||
cacheSubscription?.dispose()
|
||||
cacheSubscription = head.getFlux().subscribe {
|
||||
blocksCache.add(it)
|
||||
}
|
||||
cache = CachingEthereumApi(objectMapper, blockReader, head)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.hex.HexQuantity
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.function.Function
|
||||
|
||||
open class CachingEthereumApi(
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val cache: Reader<BlockHash, BlockJson<TransactionId>>,
|
||||
private val head: EthereumHead
|
||||
): EthereumApi(objectMapper) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun empty(): CachingEthereumApi {
|
||||
return CachingEthereumApi(ObjectMapper(), EmptyReader(), EmptyEthereumHead())
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||
return when (method) {
|
||||
"eth_blockNumber" -> head.getFlux().next()
|
||||
.map { HexQuantity.from(it.number).toHex() }
|
||||
.map(toJson(id))
|
||||
"eth_getBlockByHash" -> Mono.just(params[0])
|
||||
.map { BlockHash.from(it as String) }
|
||||
.flatMap(cache::read)
|
||||
.map(toJson(id))
|
||||
else -> Mono.empty()
|
||||
}
|
||||
}
|
||||
|
||||
fun toJson(id: Int): Function<Any, ByteArray> {
|
||||
return Function { data ->
|
||||
val resp = ResponseJson<Any, Int>()
|
||||
resp.id = id
|
||||
resp.result = data
|
||||
objectMapper.writer().writeValueAsBytes(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.Disposable
|
||||
import java.io.Closeable
|
||||
import java.lang.IllegalStateException
|
||||
import java.time.Duration
|
||||
|
||||
class ChainUpstreams (
|
||||
open class ChainUpstreams (
|
||||
val chain: Chain,
|
||||
private val upstreams: MutableList<Upstream>,
|
||||
targets: CallMethods
|
||||
) : AggregatedUpstream(targets), Lifecycle {
|
||||
|
||||
targets: CallMethods,
|
||||
objectMapper: ObjectMapper
|
||||
) : AggregatedUpstream(targets, objectMapper), Lifecycle {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
||||
private var seq = 0
|
||||
@@ -30,12 +30,14 @@ class ChainUpstreams (
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
subscription = observeStatus()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { printStatus() }
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
subscription?.dispose()
|
||||
subscription = null
|
||||
head?.let {
|
||||
@@ -54,7 +56,7 @@ class ChainUpstreams (
|
||||
}
|
||||
lagObserver?.stop()
|
||||
lagObserver = null
|
||||
return if (upstreams.size == 1) {
|
||||
val head = if (upstreams.size == 1) {
|
||||
val upstream = upstreams.first()
|
||||
upstream.setLag(0)
|
||||
upstream.getHead()
|
||||
@@ -68,6 +70,8 @@ class ChainUpstreams (
|
||||
this.lagObserver = lagObserver
|
||||
newHead
|
||||
}
|
||||
onHeadUpdated(head)
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getAll(): List<Upstream> {
|
||||
@@ -79,7 +83,7 @@ class ChainUpstreams (
|
||||
head = updateHead()
|
||||
}
|
||||
|
||||
override fun getApis(matcher: Selector.Matcher): Iterator<EthereumApi> {
|
||||
override fun getApis(matcher: Selector.Matcher): Iterator<DirectEthereumApi> {
|
||||
val i = seq++
|
||||
if (seq >= Int.MAX_VALUE / 2) {
|
||||
seq = 0
|
||||
@@ -87,7 +91,7 @@ class ChainUpstreams (
|
||||
return FilteringApiIterator(upstreams, i, matcher)
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||
override fun getApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||
return getApis(matcher).next()
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ open class ConfiguredUpstreams(
|
||||
chain: Chain,
|
||||
options: UpstreamsConfig.Options,
|
||||
labels: UpstreamsConfig.Labels) {
|
||||
var rpcApi: EthereumApi? = null
|
||||
var rpcApi: DirectEthereumApi? = null
|
||||
val urls = ArrayList<URI>()
|
||||
up.rpc?.let { endpoint ->
|
||||
val rpcTransport = DefaultRpcTransport(endpoint.url)
|
||||
@@ -117,10 +117,9 @@ open class ConfiguredUpstreams(
|
||||
}
|
||||
}
|
||||
val rpcClient = DefaultRpcClient(rpcTransport)
|
||||
rpcApi = EthereumApi(
|
||||
rpcApi = DirectEthereumApi(
|
||||
rpcClient,
|
||||
objectMapper,
|
||||
chain,
|
||||
targetFor(chain)
|
||||
)
|
||||
urls.add(endpoint.url)
|
||||
@@ -171,7 +170,7 @@ open class ConfiguredUpstreams(
|
||||
override fun addUpstream(chain: Chain, up: Upstream): ChainUpstreams {
|
||||
val current = chainMapping[chain]
|
||||
if (current == null) {
|
||||
val created = ChainUpstreams(chain, ArrayList<Upstream>(), targetFor(chain))
|
||||
val created = ChainUpstreams(chain, ArrayList<Upstream>(), targetFor(chain), objectMapper)
|
||||
created.addUpstream(up)
|
||||
created.start()
|
||||
chainMapping[chain] = created
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.infinitape.etherjar.rpc.RpcCall
|
||||
import io.infinitape.etherjar.rpc.RpcClient
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
|
||||
open class DirectEthereumApi(
|
||||
val rpcClient: RpcClient,
|
||||
private val objectMapper: ObjectMapper,
|
||||
val targets: CallMethods
|
||||
): EthereumApi(objectMapper) {
|
||||
|
||||
private val timeout = Duration.ofSeconds(5)
|
||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
||||
|
||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||
val result: Mono<out Any> = when {
|
||||
targets.isHardcoded(method) -> Mono.just(method).map { targets.hardcoded(it) }
|
||||
targets.isAllowed(method) -> callUpstream(method, params)
|
||||
else -> Mono.error(RpcException(-32601, "Method not allowed or not found"))
|
||||
}
|
||||
return result
|
||||
.doOnError { t ->
|
||||
log.warn("Upstream error: ${t.message} for ${method}")
|
||||
}
|
||||
.map {
|
||||
val resp = ResponseJson<Any, Int>()
|
||||
resp.id = id
|
||||
resp.result = it
|
||||
objectMapper.writer().writeValueAsBytes(resp)
|
||||
}
|
||||
.onErrorMap { t ->
|
||||
if (RpcException::class.java.isAssignableFrom(t.javaClass)) {
|
||||
t
|
||||
} else {
|
||||
log.warn("Convert to RPC error. Exception: ${t.message}")
|
||||
RpcException(-32020, "Error reading from upstream", null, t)
|
||||
}
|
||||
}
|
||||
.onErrorResume(RpcException::class.java) { t ->
|
||||
val resp = ResponseJson<Any, Int>()
|
||||
resp.id = id
|
||||
resp.error = t.error
|
||||
Mono.just(objectMapper.writer().writeValueAsBytes(resp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun callUpstream(method: String, params: List<Any>): Mono<out Any> {
|
||||
return Mono.fromCompletionStage(
|
||||
rpcClient.execute(RpcCall.create(method, Any::class.java, params))
|
||||
).timeout(timeout, Mono.error(RpcException(-32603, "Upstream timeout")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
class EmptyEthereumHead : EthereumHead {
|
||||
|
||||
override fun getFlux(): Flux<BlockJson<TransactionId>> {
|
||||
return Flux.empty()
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,22 @@ import io.infinitape.etherjar.rpc.*
|
||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
import java.io.InputStream
|
||||
import java.time.Duration
|
||||
|
||||
open class EthereumApi(
|
||||
val rpcClient: RpcClient,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val chain: Chain,
|
||||
val targets: CallMethods
|
||||
abstract class EthereumApi(
|
||||
objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||
var upstream: Upstream? = null
|
||||
|
||||
private val timeout = Duration.ofSeconds(5)
|
||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
||||
var ws: EthereumWs? = null
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
abstract fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
|
||||
|
||||
open fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
|
||||
fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
|
||||
val convertToJS = java.util.function.Function<ByteArray, Mono<JS>> { resp ->
|
||||
val jsonValue: JS? = jacksonRpcConverter.fromJson(resp.inputStream(), rpcCall.jsonType, Int::class.java)
|
||||
val inputStream: InputStream = resp.inputStream()
|
||||
val jsonValue: JS? = jacksonRpcConverter.fromJson(inputStream, rpcCall.jsonType, Int::class.java)
|
||||
if (jsonValue == null) Mono.empty<JS>()
|
||||
else Mono.just(jsonValue)
|
||||
}
|
||||
@@ -36,51 +30,4 @@ open class EthereumApi(
|
||||
.flatMap(convertToJS)
|
||||
.map(rpcCall.converter::apply)
|
||||
}
|
||||
|
||||
open fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||
val result: Mono<out Any> = when {
|
||||
targets.isHardcoded(method) -> Mono.just(method).map { targets.hardcoded(it) }
|
||||
targets.isAllowed(method) -> callUpstream(method, params)
|
||||
else -> Mono.error(RpcException(-32601, "Method not allowed or not found"))
|
||||
}
|
||||
return result
|
||||
.doOnError { t ->
|
||||
log.warn("Upstream error: ${t.message} for ${method} on $chain")
|
||||
}
|
||||
.map {
|
||||
val resp = ResponseJson<Any, Int>()
|
||||
resp.id = id
|
||||
resp.result = it
|
||||
objectMapper.writer().writeValueAsBytes(resp)
|
||||
}
|
||||
.onErrorMap { t ->
|
||||
if (RpcException::class.java.isAssignableFrom(t.javaClass)) {
|
||||
t
|
||||
} else {
|
||||
log.warn("Convert to RPC error. Exception: ${t.message}")
|
||||
RpcException(-32020, "Error reading from upstream", null, t)
|
||||
}
|
||||
}
|
||||
.onErrorResume(RpcException::class.java) { t ->
|
||||
val resp = ResponseJson<Any, Int>()
|
||||
resp.id = id
|
||||
resp.error = t.error
|
||||
Mono.just(objectMapper.writer().writeValueAsBytes(resp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun callUpstream(method: String, params: List<Any>): Mono<out Any> {
|
||||
if (method == "eth_blockNumber") {
|
||||
val current = upstream?.getHead()?.getFlux()?.next()?.let { head ->
|
||||
head.map { HexQuantity.from(it.number).toHex() }
|
||||
}
|
||||
if (current != null) {
|
||||
return current
|
||||
}
|
||||
}
|
||||
return Mono.fromCompletionStage(
|
||||
rpcClient.execute(RpcCall.create(method, Any::class.java, params))
|
||||
).timeout(timeout, Mono.error(RpcException(-32603, "Upstream timeout")))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import java.time.Duration
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class EthereumRpcHead(
|
||||
private val api: EthereumApi
|
||||
private val api: DirectEthereumApi
|
||||
): EthereumHead, Lifecycle {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EthereumRpcHead::class.java)
|
||||
|
||||
@@ -8,14 +8,14 @@ import reactor.core.Disposable
|
||||
|
||||
open class EthereumUpstream(
|
||||
val chain: Chain,
|
||||
private val api: EthereumApi,
|
||||
private val api: DirectEthereumApi,
|
||||
private val ethereumWs: EthereumWs? = null,
|
||||
private val options: UpstreamsConfig.Options,
|
||||
val node: NodeDetailsList.NodeDetails,
|
||||
private val targets: CallMethods
|
||||
): DefaultUpstream(), Lifecycle {
|
||||
|
||||
constructor(chain: Chain, api: EthereumApi): this(chain, api, null,
|
||||
constructor(chain: Chain, api: DirectEthereumApi): this(chain, api, null,
|
||||
UpstreamsConfig.Options.getDefaults(), NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels()),
|
||||
DirectCallMethods())
|
||||
|
||||
@@ -84,11 +84,11 @@ open class EthereumUpstream(
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||
override fun getApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||
return api
|
||||
}
|
||||
|
||||
fun getApi(): EthereumApi {
|
||||
fun getApi(): DirectEthereumApi {
|
||||
return api
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class FilteringApiIterator(
|
||||
private var pos: Int,
|
||||
private val matcher: Selector.Matcher,
|
||||
private val repeatLimit: Int = 3
|
||||
): Iterator<EthereumApi> {
|
||||
): Iterator<DirectEthereumApi> {
|
||||
|
||||
private var nextUpstream: Upstream? = null
|
||||
private var consumed = 0
|
||||
@@ -31,7 +31,7 @@ class FilteringApiIterator(
|
||||
return nextInternal()
|
||||
}
|
||||
|
||||
override fun next(): EthereumApi {
|
||||
override fun next(): DirectEthereumApi {
|
||||
if (nextInternal()) {
|
||||
val curr = nextUpstream!!
|
||||
nextUpstream = null
|
||||
|
||||
@@ -44,9 +44,9 @@ open class GrpcUpstream(
|
||||
|
||||
private var headSubscription: Disposable? = null
|
||||
|
||||
open fun createApi(matcher: Selector.Matcher): EthereumApi {
|
||||
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||
val rpcClient = DefaultRpcClient(grpcTransport.withMatcher(matcher))
|
||||
return EthereumApi(rpcClient, objectMapper, chain, targets).let {
|
||||
return DirectEthereumApi(rpcClient, objectMapper, targets).let {
|
||||
it.upstream = this
|
||||
it
|
||||
}
|
||||
@@ -156,7 +156,7 @@ open class GrpcUpstream(
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||
override fun getApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||
return createApi(matcher)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ interface Upstream {
|
||||
fun getStatus(): UpstreamAvailability
|
||||
fun observeStatus(): Flux<UpstreamAvailability>
|
||||
fun getHead(): EthereumHead
|
||||
fun getApi(matcher: Selector.Matcher): EthereumApi
|
||||
fun getApi(matcher: Selector.Matcher): DirectEthereumApi
|
||||
// fun getCache(): CachingEthereumApi
|
||||
fun getOptions(): UpstreamsConfig.Options
|
||||
fun getSupportedTargets(): Set<String>
|
||||
fun setLag(lag: Long)
|
||||
|
||||
Reference in New Issue
Block a user