Add NotNullQuorum for tx methods (#208)

This commit is contained in:
KirillPamPam
2023-04-27 15:10:57 +04:00
committed by GitHub
parent 89330da4b0
commit a5c265b5f1
17 changed files with 207 additions and 304 deletions

View File

@@ -39,6 +39,8 @@ class Global {
companion object {
val nullValue: ByteArray = "null".toByteArray()
var metricsExtended = false
val chainNames = mapOf(

View File

@@ -1,82 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class NonEmptyQuorum(
val maxTries: Int = 3
) : CallQuorum, ValueAwareQuorum<Any>(Any::class.java) {
private var result: ByteArray? = null
private var tries: Int = 0
private var sig: ResponseSigner.Signature? = null
private var providedUpstreamId: String? = null
override fun init(head: Head) {
}
override fun isResolved(): Boolean {
return result != null
}
override fun isFailed(): Boolean {
return tries >= maxTries
}
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun getProvidedUpstreamId(): String? {
return providedUpstreamId
}
override fun recordValue(
response: ByteArray,
responseValue: Any?,
signature: ResponseSigner.Signature?,
upstream: Upstream,
providedUpstreamId: String?
) {
tries++
if (responseValue != null) {
result = response
sig = signature
this.providedUpstreamId = providedUpstreamId
}
}
override fun getResult(): ByteArray? {
return result
}
override fun recordError(
response: ByteArray?,
errorMessage: String?,
sig: ResponseSigner.Signature?,
upstream: Upstream,
providedUpstreamId: String?
) {
tries++
}
override fun toString(): String {
return "Quorum: Accept Non Error Result"
}
}

View File

@@ -0,0 +1,73 @@
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
class NotNullQuorum : CallQuorum {
private var sig: ResponseSigner.Signature? = null
private var providedUpstreamId: String? = null
private var result: ByteArray? = null
private var rpcError: JsonRpcError? = null
private val resolvers = ArrayList<Upstream>()
private var allFailed = true
private val seenUpstreams = HashSet<String>() // just to prevent calling retry upstreams in FilteredApis
override fun init(head: Head) {
}
override fun isResolved(): Boolean = result != null
override fun isFailed(): Boolean = rpcError != null
override fun record(
response: ByteArray,
signature: ResponseSigner.Signature?,
upstream: Upstream,
providedUpstreamId: String?
): Boolean {
allFailed = false
val receivedNull = response.isEmpty() || Global.nullValue.contentEquals(response)
val upId = upstream.getId()
if (seenUpstreams.contains(upId) || !receivedNull) {
sig = signature
result = response
this.providedUpstreamId = providedUpstreamId
resolvers.add(upstream)
return true
}
seenUpstreams.add(upId)
return false
}
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
val upId = upstream.getId()
if (seenUpstreams.contains(upId)) {
if (allFailed) {
rpcError = error.error
} else {
result = Global.nullValue
resolvers.add(upstream)
}
sig = signature
}
seenUpstreams.add(upId)
}
override fun getSignature(): ResponseSigner.Signature? = sig
override fun getProvidedUpstreamId(): String? = providedUpstreamId
override fun getResult(): ByteArray? = result
override fun getError(): JsonRpcError? = rpcError
override fun getResolvedBy(): Collection<Upstream> = resolvers.toList()
override fun toString(): String {
return "Quorum: Not null"
}
}

View File

@@ -169,7 +169,7 @@ class QuorumRpcReader(
fun <T> withErrorResume(api: Upstream, key: JsonRpcRequest): Function<Mono<T>, Mono<T>> {
return Function { src ->
src.onErrorResume { err ->
log.error("Error during call upstream ${api.getId()} with method $${key.method}", err)
log.error("Error during call upstream ${api.getId()} with method ${key.method}", err)
// when the call failed with an error we want to notify the quorum because
// it may use the error message or other details
//

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.Global.Companion.nullValue
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.commons.LOCAL_READER
import io.emeraldpay.dshackle.commons.REMOTE_QUORUM_RPC_READER
@@ -78,8 +79,6 @@ open class NativeCall(
private val log = LoggerFactory.getLogger(NativeCall::class.java)
private val objectMapper: ObjectMapper = Global.objectMapper
private val nullValue: ByteArray = "null".toByteArray()
private val localRouterEnabled = config.cache?.requestsCacheEnabled ?: true
private val passthrough = config.passthrough

View File

@@ -19,8 +19,8 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.quorum.NotNullQuorum
import io.emeraldpay.etherjar.rpc.RpcException
import java.util.Collections
@@ -62,7 +62,7 @@ class DefaultBitcoinMethods : CallMethods {
override fun createQuorumFor(method: String): CallQuorum {
return when {
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NonEmptyQuorum()
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotNullQuorum()
Collections.binarySearch(freshMethods, method) >= 0 -> NotLaggingQuorum(2)
Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0)
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum()

View File

@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.NonceQuorum
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.quorum.NotNullQuorum
import io.emeraldpay.etherjar.rpc.RpcException
/**
@@ -77,15 +78,18 @@ class DefaultEthereumMethods(
"eth_estimateGas"
)
private val possibleNotIndexedMethods = listOf(
"eth_getTransactionByHash",
"eth_getTransactionReceipt"
)
private val firstValueMethods = listOf(
"eth_getBlockTransactionCountByHash",
"eth_getUncleCountByBlockHash",
"eth_getBlockByHash",
"eth_getBlockByNumber",
"eth_getTransactionByHash",
"eth_getTransactionByBlockHashAndIndex",
"eth_getTransactionByBlockNumberAndIndex",
"eth_getTransactionReceipt",
"eth_getStorageAt",
"eth_getCode",
"eth_getUncleByBlockHashAndIndex",
@@ -127,6 +131,7 @@ class DefaultEthereumMethods(
init {
allowedMethods = anyResponseMethods +
firstValueMethods +
possibleNotIndexedMethods +
specialMethods +
headVerifiedMethods -
chainUnsupportedMethods(chain) +
@@ -141,6 +146,7 @@ class DefaultEthereumMethods(
firstValueMethods.contains(method) -> AlwaysQuorum()
anyResponseMethods.contains(method) -> NotLaggingQuorum(4)
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
possibleNotIndexedMethods.contains(method) -> NotNullQuorum()
specialMethods.contains(method) -> {
when (method) {
"eth_getTransactionCount" -> NonceQuorum()

View File

@@ -19,8 +19,8 @@ package io.emeraldpay.dshackle.upstream.calls
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.quorum.NotNullQuorum
import org.apache.commons.collections4.Factory
import org.slf4j.LoggerFactory
import java.io.IOException
@@ -66,7 +66,7 @@ class ManagedCallMethods(
val quorum = when (quorumId) {
"always" -> Factory<CallQuorum> { AlwaysQuorum() }
"no-lag", "not-lagging", "no_lag", "not_lagging" -> Factory<CallQuorum> { NotLaggingQuorum(0) }
"not-empty", "not_empty", "non-empty", "non_empty" -> Factory<CallQuorum> { NonEmptyQuorum() }
"not-empty", "not_empty", "non-empty", "non_empty" -> Factory<CallQuorum> { NotNullQuorum() }
else -> {
log.warn("Unknown quorum: $quorumId for custom method $method")
return

View File

@@ -76,9 +76,8 @@ class EthereumDirectReader(
txReader = object : Reader<TransactionId, TxContainer> {
override fun read(key: TransactionId): Mono<TxContainer> {
val request = JsonRpcRequest("eth_getTransactionByHash", listOf(key.toHex()))
return readWithQuorum(request)
return readWithQuorum(request) // retries were removed because we use NotNullQuorum which handle errors too
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key")))
.retryWhen(Retry.fixedDelay(3, Duration.ofMillis(200)))
.flatMap { txbytes ->
val tx = objectMapper.readValue(txbytes, TransactionJson::class.java)
if (tx == null) {

View File

@@ -15,6 +15,7 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Global.Companion.nullValue
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.JsonRpcReader
@@ -27,6 +28,7 @@ import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import java.math.BigInteger
/**
@@ -88,7 +90,10 @@ class EthereumLocalReader(
} catch (e: IllegalArgumentException) {
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id")
}
reader.txByHashAsCont().read(hash).map { it.json!! }
reader.txByHashAsCont()
.read(hash)
.map { it.json!! }
.switchIfEmpty { Mono.just(nullValue) }
}
method == "eth_getBlockByHash" -> {
if (params.size != 2) {
@@ -120,7 +125,9 @@ class EthereumLocalReader(
} catch (e: IllegalArgumentException) {
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id")
}
reader.receipts().read(hash)
reader.receipts()
.read(hash)
.switchIfEmpty { Mono.just(nullValue) }
}
else -> null
}