Merge pull request #67 from p2p-org/fix-managed-methods-from-upstream

fix managed methods reusing old quorums
This commit is contained in:
Vyacheslav Shebanov
2022-12-07 12:45:20 +02:00
committed by GitHub
13 changed files with 66 additions and 35 deletions

View File

@@ -229,7 +229,7 @@ open class NativeCall(
.forMethod(method) .forMethod(method)
.forLabels(Selector.convertToMatcher(request.selector)) .forLabels(Selector.convertToMatcher(request.selector))
val callQuorum = availableMethods.getQuorumFor(method) // can be null in tests val callQuorum = availableMethods.createQuorumFor(method) // can be null in tests
callQuorum.init(upstream.getHead()) callQuorum.init(upstream.getHead())
// for NotLaggingQuorum it makes sense to select compatible upstreams before the call // for NotLaggingQuorum it makes sense to select compatible upstreams before the call

View File

@@ -37,10 +37,10 @@ class AggregatedCallMethods(
/** /**
* Finds first delegate that has Allowed that method and returns its Quorum * Finds first delegate that has Allowed that method and returns its Quorum
*/ */
override fun getQuorumFor(method: String): CallQuorum { override fun createQuorumFor(method: String): CallQuorum {
return delegates.find { return delegates.find {
it.isCallable(method) || it.isHardcoded(method) it.isCallable(method) || it.isHardcoded(method)
}?.getQuorumFor(method) ?: throw IllegalStateException("No executor delegate for $method") }?.createQuorumFor(method) ?: throw IllegalStateException("No executor delegate for $method")
} }
/** /**

View File

@@ -24,9 +24,11 @@ import io.emeraldpay.dshackle.quorum.CallQuorum
interface CallMethods { interface CallMethods {
/** /**
* For a stateful CallQuorum it _MUST CREATE_ a new instance of each time to avoid using a shared state between different requests
*
* @return CallQuorum configured for the specified method * @return CallQuorum configured for the specified method
*/ */
fun getQuorumFor(method: String): CallQuorum fun createQuorumFor(method: String): CallQuorum
/** /**
* Check if the method can be called on an upstream. Doesn't include Hardcoded methods * Check if the method can be called on an upstream. Doesn't include Hardcoded methods

View File

@@ -59,7 +59,7 @@ class DefaultBitcoinMethods : CallMethods {
private val allowedMethods = private val allowedMethods =
(freshMethods + anyResponseMethods + headVerifiedMethods + broadcastMethods).sorted() (freshMethods + anyResponseMethods + headVerifiedMethods + broadcastMethods).sorted()
override fun getQuorumFor(method: String): CallQuorum { override fun createQuorumFor(method: String): CallQuorum {
return when { return when {
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum() Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NonEmptyQuorum() Collections.binarySearch(anyResponseMethods, method) >= 0 -> NonEmptyQuorum()

View File

@@ -112,7 +112,7 @@ class DefaultEthereumMethods(
getChainSpecificMethods(chain) getChainSpecificMethods(chain)
} }
override fun getQuorumFor(method: String): CallQuorum { override fun createQuorumFor(method: String): CallQuorum {
return when { return when {
filterMethods.contains(method) -> NotLaggingQuorum(1) filterMethods.contains(method) -> NotLaggingQuorum(1)
hardcodedMethods.contains(method) -> AlwaysQuorum() hardcodedMethods.contains(method) -> AlwaysQuorum()

View File

@@ -28,7 +28,7 @@ open class DirectCallMethods(private val methods: Set<String>) : CallMethods {
constructor() : this(emptySet()) constructor() : this(emptySet())
constructor(methods: Collection<String>) : this(methods.toSet()) constructor(methods: Collection<String>) : this(methods.toSet())
override fun getQuorumFor(method: String): CallQuorum { override fun createQuorumFor(method: String): CallQuorum {
return AlwaysQuorum() return AlwaysQuorum()
} }

View File

@@ -42,7 +42,7 @@ class EthereumCallSelector(
"eth_getBalance", "eth_getBalance",
"eth_getCode", "eth_getCode",
"eth_getTransactionCount", "eth_getTransactionCount",
// no "eth_getStorageAt" because it's has different structure, and therefore separate logic // no "eth_getStorageAt" because it has different structure, and therefore separate logic
"eth_call" "eth_call"
).sorted() ).sorted()
} }

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import org.apache.commons.collections4.Factory
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.util.Collections import java.util.Collections
@@ -38,14 +39,16 @@ class ManagedCallMethods(
companion object { companion object {
private val log = LoggerFactory.getLogger(ManagedCallMethods::class.java) private val log = LoggerFactory.getLogger(ManagedCallMethods::class.java)
private val defaultQuorum = AlwaysQuorum() private val defaultQuorum: Factory<CallQuorum> = Factory<CallQuorum> {
AlwaysQuorum()
}
} }
private val delegated = delegate.getSupportedMethods().sorted() private val delegated = delegate.getSupportedMethods().sorted()
private val allAllowed: Set<String> = Collections.unmodifiableSet( private val allAllowed: Set<String> = Collections.unmodifiableSet(
enabled + delegated - disabled enabled + delegated - disabled
) )
private val quorum: MutableMap<String, CallQuorum> = HashMap() private val quorum: MutableMap<String, Factory<CallQuorum>> = HashMap()
private val staticResponse: MutableMap<String, String> = HashMap() private val staticResponse: MutableMap<String, String> = HashMap()
private val redefined = delegated.filter(enabled::contains).sorted() private val redefined = delegated.filter(enabled::contains).sorted()
@@ -57,9 +60,9 @@ class ManagedCallMethods(
fun setQuorum(method: String, quorumId: String) { fun setQuorum(method: String, quorumId: String) {
val quorum = when (quorumId) { val quorum = when (quorumId) {
"always" -> AlwaysQuorum() "always" -> Factory<CallQuorum> { AlwaysQuorum() }
"no-lag", "not-lagging", "no_lag", "not_lagging" -> NotLaggingQuorum(0) "no-lag", "not-lagging", "no_lag", "not_lagging" -> Factory<CallQuorum> { NotLaggingQuorum(0) }
"not-empty", "not_empty", "non-empty", "non_empty" -> NonEmptyQuorum() "not-empty", "not_empty", "non-empty", "non_empty" -> Factory<CallQuorum> { NonEmptyQuorum() }
else -> { else -> {
log.warn("Unknown quorum: $quorumId for custom method $method") log.warn("Unknown quorum: $quorumId for custom method $method")
return return
@@ -72,13 +75,13 @@ class ManagedCallMethods(
this.staticResponse[method] = response this.staticResponse[method] = response
} }
override fun getQuorumFor(method: String): CallQuorum { override fun createQuorumFor(method: String): CallQuorum {
return when { return when {
isDelegated(method) && !isRedefined(method) -> delegate.getQuorumFor(method) isDelegated(method) && !isRedefined(method) -> delegate.createQuorumFor(method)
enabled.contains(method) -> quorum[method] ?: defaultQuorum enabled.contains(method) -> quorum[method]?.create() ?: defaultQuorum.create()
else -> { else -> {
log.warn("Getting quorum for unknown method") log.warn("Getting quorum for unknown method")
defaultQuorum defaultQuorum.create()
} }
} }
} }

View File

@@ -132,8 +132,12 @@ class EthereumDirectReader(
*/ */
private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> { private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> {
return quorumReaderFactory return quorumReaderFactory
// we do not use Signer for internal requests because it doesn't make much sense .create(
.create(up.getApiSource(Selector.empty), callMethodsFactory.create().getQuorumFor(request.method), null) up.getApiSource(Selector.empty),
callMethodsFactory.create().createQuorumFor(request.method),
// we do not use Signer for internal requests because it doesn't make much sense
null
)
.read(request) .read(request)
.map { it.value } .map { it.value }
} }

View File

@@ -1,7 +1,6 @@
package io.emeraldpay.dshackle.startup package io.emeraldpay.dshackle.startup
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
@@ -36,7 +35,7 @@ class ConfiguredUpstreamsSpec extends Specification {
def act = configurer.buildMethods(upstream, Chain.ETHEREUM) def act = configurer.buildMethods(upstream, Chain.ETHEREUM)
then: then:
act instanceof ManagedCallMethods act instanceof ManagedCallMethods
act.getQuorumFor("foo_bar") instanceof NonEmptyQuorum act.createQuorumFor("foo_bar") instanceof NonEmptyQuorum
} }
def "Got static response from extra methods"() { def "Got static response from extra methods"() {

View File

@@ -58,9 +58,9 @@ class MultistreamSpec extends Specification {
act.isCallable("eth_test1") act.isCallable("eth_test1")
act.isCallable("eth_test2") act.isCallable("eth_test2")
act.isCallable("eth_test3") act.isCallable("eth_test3")
act.getQuorumFor("eth_test1") instanceof AlwaysQuorum act.createQuorumFor("eth_test1") instanceof AlwaysQuorum
act.getQuorumFor("eth_test2") instanceof AlwaysQuorum act.createQuorumFor("eth_test2") instanceof AlwaysQuorum
act.getQuorumFor("eth_test3") instanceof AlwaysQuorum act.createQuorumFor("eth_test3") instanceof AlwaysQuorum
} }
def "Filter Best Status accepts any input when none available "() { def "Filter Best Status accepts any input when none available "() {

View File

@@ -31,11 +31,11 @@ class AggregatedCallMethodsSpec extends Specification {
def delegate2 = Mock(CallMethods) { def delegate2 = Mock(CallMethods) {
_ * getSupportedMethods() >> ["eth_test", "foo_bar"] _ * getSupportedMethods() >> ["eth_test", "foo_bar"]
1 * isCallable("eth_test") >> true 1 * isCallable("eth_test") >> true
1 * getQuorumFor("eth_test") >> quorum 1 * createQuorumFor("eth_test") >> quorum
} }
def aggregate = new AggregatedCallMethods([delegate1, delegate2]) def aggregate = new AggregatedCallMethods([delegate1, delegate2])
when: when:
def act = aggregate.getQuorumFor("eth_test") def act = aggregate.createQuorumFor("eth_test")
then: then:
act == quorum act == quorum
} }

View File

@@ -18,14 +18,15 @@ package io.emeraldpay.dshackle.upstream.calls
import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.BroadcastQuorum import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.quorum.NonceQuorum
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import spock.lang.Specification import spock.lang.Specification
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class ManagedCallMethodsSpec extends Specification { class ManagedCallMethodsSpec extends Specification {
def "Gets quorum for enabled method"() { def "Gets quorum for enabled method"() {
@@ -36,7 +37,7 @@ class ManagedCallMethodsSpec extends Specification {
[] as Set [] as Set
) )
when: when:
def act = managed.getQuorumFor("eth_test") def act = managed.createQuorumFor("eth_test")
then: then:
act instanceof AlwaysQuorum act instanceof AlwaysQuorum
} }
@@ -72,7 +73,7 @@ class ManagedCallMethodsSpec extends Specification {
def delegated = ["eth_test", "eth_test2"] as Set def delegated = ["eth_test", "eth_test2"] as Set
def delegate = Mock(CallMethods) { def delegate = Mock(CallMethods) {
_ * it.getSupportedMethods() >> delegated _ * it.getSupportedMethods() >> delegated
0 * it.getQuorumFor("eth_test") >> new BroadcastQuorum() 0 * it.createQuorumFor("eth_test") >> new BroadcastQuorum()
} }
def managed = new ManagedCallMethods( def managed = new ManagedCallMethods(
delegate, delegate,
@@ -80,7 +81,7 @@ class ManagedCallMethodsSpec extends Specification {
["foo_bar"] as Set ["foo_bar"] as Set
) )
when: when:
def act = managed.getQuorumFor("eth_test") def act = managed.createQuorumFor("eth_test")
then: then:
act != null act != null
act instanceof AlwaysQuorum act instanceof AlwaysQuorum
@@ -96,18 +97,40 @@ class ManagedCallMethodsSpec extends Specification {
managed.setQuorum("eth_test", "not_empty") managed.setQuorum("eth_test", "not_empty")
managed.setQuorum("eth_foo", "not_lagging") managed.setQuorum("eth_foo", "not_lagging")
when: when:
def act = managed.getQuorumFor("eth_test") def act = managed.createQuorumFor("eth_test")
then: then:
act instanceof NonEmptyQuorum act instanceof NonEmptyQuorum
when: when:
act = managed.getQuorumFor("eth_foo") act = managed.createQuorumFor("eth_foo")
then: then:
act instanceof NotLaggingQuorum act instanceof NotLaggingQuorum
when: when:
act = managed.getQuorumFor("eth_bar") act = managed.createQuorumFor("eth_bar")
then: then:
act instanceof AlwaysQuorum act instanceof AlwaysQuorum
} }
def "Doesn't reuse same instance"() {
def managed = new ManagedCallMethods(
new DefaultEthereumMethods(Chain.ETHEREUM),
["eth_test"] as Set,
[] as Set
)
def parallel = Executors.newFixedThreadPool(16)
when:
List<CallQuorum> instances = []
50.times {
instances << managed.createQuorumFor("eth_test")
}
parallel.shutdown()
parallel.awaitTermination(5, TimeUnit.SECONDS)
def ids = instances.collect { System.identityHashCode(it) }
then:
instances.size() == 50
ids.toSet().size() == 50
}
} }