problem: suboptimal routing for upstreams in different datacenters

solution: additional role to separate primary and secondary upstreams
fix: #141
This commit is contained in:
Igor Artamonov
2022-01-05 18:42:35 -05:00
parent 45237592a3
commit c060aeda31
16 changed files with 179 additions and 49 deletions

View File

@@ -343,8 +343,8 @@ class UpstreamsConfigReaderSpec extends Specification {
then:
act != null
act.upstreams.size() == 2
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.STANDARD
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.STANDARD
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.PRIMARY
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.PRIMARY
}
def "Parse config with fallback role"() {
@@ -355,10 +355,23 @@ class UpstreamsConfigReaderSpec extends Specification {
then:
act != null
act.upstreams.size() == 2
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.STANDARD
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.PRIMARY
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.FALLBACK
}
def "Parse config with secondary role"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-2.yaml")
when:
def act = reader.read(config)
then:
act != null
act.upstreams.size() == 3
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.PRIMARY
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.SECONDARY
act.upstreams.get(2).role == UpstreamsConfig.UpstreamRole.FALLBACK
}
def "Parse config with invalid role"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-invalid.yaml")
@@ -367,7 +380,7 @@ class UpstreamsConfigReaderSpec extends Specification {
then:
act != null
act.upstreams.size() == 2
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.STANDARD
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.STANDARD
act.upstreams.get(0).role == UpstreamsConfig.UpstreamRole.PRIMARY
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.PRIMARY
}
}

View File

@@ -38,7 +38,7 @@ class QuorumRpcReaderSpec extends Specification {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
1 * getApi() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1"))
}
@@ -71,7 +71,7 @@ class QuorumRpcReaderSpec extends Specification {
}
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> api
}
def apis = new FilteredApis(
@@ -98,7 +98,7 @@ class QuorumRpcReaderSpec extends Specification {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> Mock(Reader) {
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
@@ -130,7 +130,7 @@ class QuorumRpcReaderSpec extends Specification {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> Mock(Reader) {
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.error(1, "test")),
@@ -161,7 +161,7 @@ class QuorumRpcReaderSpec extends Specification {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> Mock(Reader) {
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
@@ -200,7 +200,7 @@ class QuorumRpcReaderSpec extends Specification {
}
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> api
}
def apis = new FilteredApis(
@@ -226,7 +226,7 @@ class QuorumRpcReaderSpec extends Specification {
def up = Mock(Upstream) {
_ * getLag() >> 0
_ * isAvailable() >> true
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getApi() >> Mock(Reader) {
_ * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.error(-3010, "test")),

View File

@@ -63,7 +63,7 @@ class EthereumUpstreamMock extends EthereumRpcUpstream {
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
super(id, chain, api, null,
UpstreamsConfig.Options.getDefaults(),
UpstreamsConfig.UpstreamRole.STANDARD,
UpstreamsConfig.UpstreamRole.PRIMARY,
new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
methods)
setLag(0)

View File

@@ -52,7 +52,7 @@ class FilteredApisSpec extends Specification {
TestingCommons.api().tap { it.id = "${i++}" },
(EthereumWsFactory) null,
new UpstreamsConfig.Options(),
UpstreamsConfig.UpstreamRole.STANDARD,
UpstreamsConfig.UpstreamRole.PRIMARY,
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)),
ethereumTargets
)
@@ -210,7 +210,7 @@ class FilteredApisSpec extends Specification {
6 | [0, 1]
}
def "Starts with standard"() {
def "Starts with primary"() {
setup:
List<Upstream> standard = (0..1).collect {
TestingCommons.upstream(
@@ -241,4 +241,45 @@ class FilteredApisSpec extends Specification {
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Use secondary after primary"() {
setup:
List<Upstream> standard = (0..1).collect {
TestingCommons.upstream(
it.toString(),
new EthereumApiStub(it)
)
}
List<Upstream> fallback = [
Mock([name: "fallback"], Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.FALLBACK
_ * isAvailable() >> true
}
]
List<Upstream> secondary = [
Mock([name: "secondary"], Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.SECONDARY
_ * isAvailable() >> true
}
]
when:
def act = new FilteredApis(Chain.ETHEREUM,
[] + fallback + standard + secondary,
Selector.empty, 0, 3, 0)
act.request(11)
then:
StepVerifier.create(act)
.expectNext(standard[0], standard[1]).as("Initial requests with primary")
.expectNext(secondary[0]).as("Initial requests with secondary")
.expectNext(standard[0], standard[1]).as("Retry with primary")
.expectNext(secondary[0]).as("Retry with secondary")
.expectNext(fallback[0]).as("Retry with fallback")
.expectNext(standard[0], standard[1]).as("Second retry with primary")
.expectNext(secondary[0]).as("Second retry with secondary")
.expectNext(fallback[0]).as("Second retry with fallback")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.test.MockGrpcServer
import io.emeraldpay.dshackle.test.TestingCommons
@@ -80,7 +81,7 @@ class EthereumGrpcUpstreamSpec extends Specification {
)
}
})
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain, metrics))
def upstream = new EthereumGrpcUpstream("test", UpstreamsConfig.UpstreamRole.PRIMARY, chain, client, new JsonRpcGrpcClient(client, chain, metrics))
upstream.setLag(0)
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
@@ -138,7 +139,7 @@ class EthereumGrpcUpstreamSpec extends Specification {
)
}
})
def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, new JsonRpcGrpcClient(client, Chain.ETHEREUM, metrics))
def upstream = new EthereumGrpcUpstream("test", UpstreamsConfig.UpstreamRole.PRIMARY, Chain.ETHEREUM, client, new JsonRpcGrpcClient(client, Chain.ETHEREUM, metrics))
upstream.setLag(0)
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
@@ -200,7 +201,7 @@ class EthereumGrpcUpstreamSpec extends Specification {
finished.complete(true)
}
})
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain, metrics))
def upstream = new EthereumGrpcUpstream("test", UpstreamsConfig.UpstreamRole.PRIMARY, chain, client, new JsonRpcGrpcClient(client, chain, metrics))
upstream.setLag(0)
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))

View File

@@ -0,0 +1,44 @@
version: v1
defaults:
- chains:
- ethereum
options:
min-peers: 3
upstreams:
- id: local
chain: ethereum
connection:
ethereum:
rpc:
url: "http://localhost:8545"
ws:
url: "ws://localhost:8546"
origin: "http://localhost"
basic-auth:
username: 9c199ad8f281f20154fc258fe41a6814
password: 258fe4149c199ad8f2811a68f20154fc
- id: closeby
chain: ethereum
role: secondary
connection:
ethereum:
rpc:
url: "http://localhost:8545"
ws:
url: "ws://localhost:8546"
origin: "http://localhost"
basic-auth:
username: 9c199ad8f281f20154fc258fe41a6814
password: 258fe4149c199ad8f2811a68f20154fc
- id: infura
chain: ethereum
role: fallback
connection:
ethereum:
rpc:
url: "https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2"
basic-auth:
username: 4fc258fe41a68149c199ad8f281f2015
password: 1a68f20154fc258fe4149c199ad8f281