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

@@ -1,129 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* 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.rpcclient.JsonRpcException
import spock.lang.Specification
class NonEmptyQuorumSpec extends Specification {
def "Fail if too many errors"() {
setup:
def q = Spy(new NonEmptyQuorum(3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
then:
q.isFailed()
!q.isResolved()
q.signature == null
}
def "Fail first if not error"() {
setup:
def q = Spy(new NonEmptyQuorum(3))
def upstream1 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record('"0x11"'.bytes, null, upstream1, null)
then:
q.isResolved()
!q.isFailed()
}
def "Fail second if first is error"() {
setup:
def q = Spy(new NonEmptyQuorum(3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isFailed()
!q.isResolved()
q.signature == null
when:
q.record('"0x11"'.bytes, null, upstream2, null)
then:
q.isResolved()
!q.isFailed()
}
def "Fail second if first is null"() {
setup:
def q = Spy(new NonEmptyQuorum(3))
def upstream1 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record('null'.bytes, null, upstream1, null)
then:
!q.isFailed()
!q.isResolved()
when:
q.record('"0x11"'.bytes, null, upstream1, null)
then:
q.isResolved()
!q.isFailed()
}
}

View File

@@ -0,0 +1,93 @@
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import spock.lang.Specification
class NotNullQuorumSpec extends Specification {
def "Resolves if attempts are exhausted and response is null"() {
setup:
def up = Mock(Upstream) {
2 * getId() >> "id"
}
def up1 = Mock(Upstream) {
1 * getId() >> "id1"
}
def up2 = Mock(Upstream) {
1 * getId() >> "id2"
}
def value = "null".getBytes()
def quorum = new NotNullQuorum()
when:
def res = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up, "id")
def res1 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up1, "id1")
def res2 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up2, "id2")
def res3 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up, "id")
then:
!res
!res1
!res2
res3
quorum.result == value
!quorum.isFailed()
quorum.isResolved()
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
quorum.providedUpstreamId == "id"
}
def "Failed if all upstreams respond with error"() {
setup:
def up = Mock(Upstream) {
2 * getId() >> "id"
}
def up1 = Mock(Upstream) {
1 * getId() >> "id1"
}
def up2 = Mock(Upstream) {
1 * getId() >> "id2"
}
def quorum = new NotNullQuorum()
when:
quorum.record(new JsonRpcException(10, "error"), null, up)
quorum.record(new JsonRpcException(10, "error"), null, up1)
quorum.record(new JsonRpcException(10, "error"), null, up2)
quorum.record(new JsonRpcException(10, "error"), null, up)
then:
quorum.isFailed()
!quorum.isResolved()
quorum.error == new JsonRpcException(10, "error").error
}
def "Resolve if one of upstream responds with value"() {
setup:
def up = Mock(Upstream) {
2 * getId() >> "id"
}
def up1 = Mock(Upstream) {
1 * getId() >> "id1"
}
def up2 = Mock(Upstream) {
1 * getId() >> "id2"
}
def value = "null".getBytes()
def quorum = new NotNullQuorum()
when:
def res = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up, "id")
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up1)
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up2)
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then:
!res
quorum.isResolved()
!quorum.isFailed()
quorum.result == value
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
}
}

View File

@@ -148,7 +148,7 @@ class QuorumRpcReaderSpec extends Specification {
Chain.ETHEREUM,
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(3), Stub(Tracer))
def reader = new QuorumRpcReader(apis, new NotNullQuorum(), Stub(Tracer))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
@@ -180,39 +180,7 @@ class QuorumRpcReaderSpec extends Specification {
Chain.ETHEREUM,
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(3), Stub(Tracer))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "non-empty-quorum - get the third result if first two are not ok"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getIngressReader() >> Mock(Reader) {
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("1"))
]
}
}
def apis = new FilteredApis(
Chain.ETHEREUM,
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(3), Stub(Tracer))
def reader = new QuorumRpcReader(apis, new NotNullQuorum(), Stub(Tracer))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
@@ -230,13 +198,13 @@ class QuorumRpcReaderSpec extends Specification {
def "non-empty-quorum - error if all failed"() {
setup:
def api = Mock(Reader) {
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("null"))
]
}
def up = Mock(Upstream) {
_ * getId() >> "test"
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getIngressReader() >> api
@@ -245,7 +213,7 @@ class QuorumRpcReaderSpec extends Specification {
Chain.ETHEREUM,
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(3), Stub(Tracer))
def reader = new QuorumRpcReader(apis, new NotNullQuorum(), Stub(Tracer))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))

View File

@@ -16,7 +16,6 @@
*/
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Chain
@@ -141,6 +140,7 @@ class TrackEthereumTxSpec extends Specification {
act
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(20)).as("Waited for updates")
.thenAwait(Duration.ofSeconds(2))
.expectComplete()
.verify(Duration.ofSeconds(3))
}
@@ -171,18 +171,15 @@ class TrackEthereumTxSpec extends Specification {
def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock)
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
def scheduler = VirtualTimeScheduler.create(true)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, scheduler)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.boundedElastic())
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
apiMock.answer("eth_getTransactionByHash", [txId], null, 2)
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
when:
def act = StepVerifier.withVirtualTime({
return trackTx.subscribe(req).take(2)
}, { scheduler }, 5)
def act = trackTx.subscribe(req).take(2)
then:
act
StepVerifier.create(act)
.expectSubscription()
.expectNext(exp1).as("Unknown tx")
.expectNext(exp2).as("Found in mempool")

View File

@@ -7,7 +7,7 @@ import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.CompressionConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.quorum.NotNullQuorum
import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import org.springframework.context.ApplicationEventPublisher
@@ -47,7 +47,7 @@ class ConfiguredUpstreamsSpec extends Specification {
def act = configurer.buildMethods(upstream, Chain.ETHEREUM)
then:
act instanceof ManagedCallMethods
act.createQuorumFor("foo_bar") instanceof NonEmptyQuorum
act.createQuorumFor("foo_bar") instanceof NotNullQuorum
}
def "Got static response from extra methods"() {

View File

@@ -17,11 +17,7 @@
package io.emeraldpay.dshackle.upstream.calls
import io.emeraldpay.dshackle.Chain
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.*
import spock.lang.Specification
import java.util.concurrent.Executors
@@ -109,7 +105,7 @@ class ManagedCallMethodsSpec extends Specification {
when:
def act = managed.createQuorumFor("eth_test")
then:
act instanceof NonEmptyQuorum
act instanceof NotNullQuorum
when:
act = managed.createQuorumFor("eth_foo")

View File

@@ -427,32 +427,6 @@ class EthereumDirectReaderSpec extends Specification {
.verify(Duration.ofSeconds(1))
}
def "Reads tx by hash with retries - expects an error within 1 sec"() {
setup:
def up = Mock(Multistream) {
4 * getApiSource(_) >> Stub(ApiSource)
}
def calls = Mock(Factory) {
4 * create() >> new DefaultEthereumMethods(Chain.ETHEREUM)
}
EthereumDirectReader reader = new EthereumDirectReader(
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
4 * create(_, _, _, _) >> Mock(Reader) {
4 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >>>
[Mono.error(new RuntimeException()), Mono.error(new RuntimeException()),
Mono.error(new RuntimeException()), Mono.error(new RuntimeException())]
}
}
when:
def act = reader.txReader.read(TransactionId.from(hash1))
then:
StepVerifier.create(act)
.expectError()
.verify(Duration.ofSeconds(1))
}
def "Reads balance with retries - expects an error within 1 sec"() {
setup:
def up = Mock(Multistream) {