problem: suboptimal routing for upstreams in different datacenters
solution: additional role to separate primary and secondary upstreams fix: #141
This commit is contained in:
@@ -74,7 +74,7 @@ cluster:
|
|||||||
password: ${INFURA_PASSWD}
|
password: ${INFURA_PASSWD}
|
||||||
----
|
----
|
||||||
|
|
||||||
There're two main segments for upstreams configuration:
|
There are two main segments for upstreams configuration:
|
||||||
|
|
||||||
- _upstreams_ - a list of API to connect to, with all configuration specific for upstream and chain
|
- _upstreams_ - a list of API to connect to, with all configuration specific for upstream and chain
|
||||||
- and _default options_ as common configuration options applied to all nodes in that group
|
- and _default options_ as common configuration options applied to all nodes in that group
|
||||||
@@ -94,24 +94,33 @@ In the example above we have:
|
|||||||
** label `[provider: infura]` is set for that particular upstream, which can be selected during a request.For example for some requests you may want to use nodes with that label only, i.e. _"send that tx to infura nodes only"_, or _"read only from archive node, with label [archive: true]"_
|
** label `[provider: infura]` is set for that particular upstream, which can be selected during a request.For example for some requests you may want to use nodes with that label only, i.e. _"send that tx to infura nodes only"_, or _"read only from archive node, with label [archive: true]"_
|
||||||
** upstream validation (peers, sync status, etc) is disabled for that particular upstream
|
** upstream validation (peers, sync status, etc) is disabled for that particular upstream
|
||||||
|
|
||||||
=== Fallback upstream
|
=== Roles and Fallback upstream
|
||||||
|
|
||||||
By default, the Dshackle connects to each upstream in a Round-Robin basis, i.e. sequentially one by one.
|
By default, the Dshackle connects to each upstream in a Round-Robin basis, i.e. sequentially one by one.
|
||||||
But if an upstream have `role: fallback` then it's used only in additional to other (_standard_) upstreams when their responses are not enough to finalize the request.
|
If you need more gradual control over the order of which upstream is used and when you can assign following roles:
|
||||||
|
|
||||||
Dshackle always starts with making requests to standard upstreams.
|
- `primary` (default role if nothing specified)
|
||||||
If all of them failed, if responses are inconsistent (ex. for `eth_getTransactionCount`), or when it needs to broadcast to wider networks (`sendrawtransaction`), then upstreams with role `fallback` are also used.
|
- `secondary`
|
||||||
|
- `fallback`
|
||||||
|
|
||||||
The internal request order is:
|
Where `primary` and `secondary` are considered here a _standard_ upstreams, and `fallback` is used on failure of standard upstreams.
|
||||||
|
I.e. the Dshackle always starts with making requests to standard upstreams.
|
||||||
|
If all of them failed, if responses are inconsistent (ex. for `eth_getTransactionCount`), or when it needs to broadcast to a wider network (`sendrawtransaction`), then upstreams with role `fallback` cames to use.
|
||||||
|
|
||||||
1. connect to each standard upstream
|
The internal request order is (goes to next only if all upstreams on current step a not available or failed):
|
||||||
2. delay
|
|
||||||
3. try again to connect to standard upstreams
|
|
||||||
4. try to connect to fallback upstreams
|
|
||||||
|
|
||||||
Steps 2-4 are repeated until a valid response received, or timeout for the original request is reached.
|
1. tries with primary upstreams
|
||||||
|
2. tries with secondary upstream
|
||||||
|
3. ... delay (100ms at first, increased each iteration)
|
||||||
|
4. tries with primary upstreams
|
||||||
|
5. tries with secondary upstream
|
||||||
|
6. tries with fallback upstreams
|
||||||
|
7. ... go to step 3
|
||||||
|
|
||||||
In general, you set role `fallback` only to external nodes provided by a third party, when you want to use it as a last resort.
|
Steps 3-6 are repeated until a valid response received, or a timeout for the original request is reached.
|
||||||
|
|
||||||
|
In general:
|
||||||
|
- you set role `secondary` for upstream in another cluster/datacenter - you set role `fallback` for an external upstream which may be provided by a third party, and you want to use it as a last resort
|
||||||
|
|
||||||
=== Configuration options
|
=== Configuration options
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ open class UpstreamsConfig {
|
|||||||
var connection: T? = null
|
var connection: T? = null
|
||||||
val labels = Labels()
|
val labels = Labels()
|
||||||
var methods: Methods? = null
|
var methods: Methods? = null
|
||||||
var role: UpstreamRole = UpstreamRole.STANDARD
|
var role: UpstreamRole = UpstreamRole.PRIMARY
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
||||||
@@ -87,7 +87,8 @@ open class UpstreamsConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class UpstreamRole {
|
enum class UpstreamRole {
|
||||||
STANDARD,
|
PRIMARY,
|
||||||
|
SECONDARY,
|
||||||
FALLBACK
|
FALLBACK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,10 @@ class UpstreamsConfigReader(
|
|||||||
internal fun readUpstreamStandard(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) {
|
internal fun readUpstreamStandard(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) {
|
||||||
upstream.chain = getValueAsString(upNode, "chain")
|
upstream.chain = getValueAsString(upNode, "chain")
|
||||||
getValueAsString(upNode, "role")?.let {
|
getValueAsString(upNode, "role")?.let {
|
||||||
val name = it.trim()
|
val name = it.trim().let {
|
||||||
|
// `standard` was initial role, now split into `primary` and `secondary`
|
||||||
|
if (it == "standard") "primary" else it
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val role = UpstreamsConfig.UpstreamRole.valueOf(name.uppercase(Locale.getDefault()))
|
val role = UpstreamsConfig.UpstreamRole.valueOf(name.uppercase(Locale.getDefault()))
|
||||||
upstream.role = role
|
upstream.role = role
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ open class ConfiguredUpstreams(
|
|||||||
val endpoint = config.connection!!
|
val endpoint = config.connection!!
|
||||||
val ds = GrpcUpstreams(
|
val ds = GrpcUpstreams(
|
||||||
config.id!!,
|
config.id!!,
|
||||||
|
config.role,
|
||||||
endpoint.host!!,
|
endpoint.host!!,
|
||||||
endpoint.port,
|
endpoint.port,
|
||||||
endpoint.auth,
|
endpoint.auth,
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ class FilteredApis(
|
|||||||
) : this(chain, allUpstreams, matcher, 0, 10, 10)
|
) : this(chain, allUpstreams, matcher, 0, 10, 10)
|
||||||
|
|
||||||
private val delay: Int
|
private val delay: Int
|
||||||
private val standardUpstreams: List<Upstream>
|
private val primaryUpstreams: List<Upstream>
|
||||||
|
private val secondaryUpstreams: List<Upstream>
|
||||||
private val standardWithFallback: List<Upstream>
|
private val standardWithFallback: List<Upstream>
|
||||||
|
|
||||||
private val control = Sinks.many().unicast().onBackpressureBuffer<Boolean>()
|
private val control = Sinks.many().unicast().onBackpressureBuffer<Boolean>()
|
||||||
@@ -94,8 +95,13 @@ class FilteredApis(
|
|||||||
DEFAULT_DELAY_STEP
|
DEFAULT_DELAY_STEP
|
||||||
}
|
}
|
||||||
|
|
||||||
standardUpstreams = allUpstreams.filter {
|
primaryUpstreams = allUpstreams.filter {
|
||||||
it.getRole() == UpstreamsConfig.UpstreamRole.STANDARD
|
it.getRole() == UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
|
}.let {
|
||||||
|
startFrom(it, pos)
|
||||||
|
}
|
||||||
|
secondaryUpstreams = allUpstreams.filter {
|
||||||
|
it.getRole() == UpstreamsConfig.UpstreamRole.SECONDARY
|
||||||
}.let {
|
}.let {
|
||||||
startFrom(it, pos)
|
startFrom(it, pos)
|
||||||
}
|
}
|
||||||
@@ -105,12 +111,14 @@ class FilteredApis(
|
|||||||
startFrom(it, pos)
|
startFrom(it, pos)
|
||||||
}
|
}
|
||||||
standardWithFallback = emptyList<Upstream>()
|
standardWithFallback = emptyList<Upstream>()
|
||||||
.plus(standardUpstreams)
|
.plus(primaryUpstreams)
|
||||||
|
.plus(secondaryUpstreams)
|
||||||
.plus(fallbackUpstreams)
|
.plus(fallbackUpstreams)
|
||||||
|
|
||||||
if (Global.metricsExtended) {
|
if (Global.metricsExtended) {
|
||||||
getMetrics(chain).let { monitoring ->
|
getMetrics(chain).let { monitoring ->
|
||||||
monitoring.countStd.record(standardUpstreams.size.toDouble())
|
monitoring.countPrimary.record(primaryUpstreams.size.toDouble())
|
||||||
|
monitoring.countSecondary.record(secondaryUpstreams.size.toDouble())
|
||||||
monitoring.countFallback.record(fallbackUpstreams.size.toDouble())
|
monitoring.countFallback.record(fallbackUpstreams.size.toDouble())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,17 +153,18 @@ class FilteredApis(
|
|||||||
|
|
||||||
override fun subscribe(subscriber: Subscriber<in Upstream>) {
|
override fun subscribe(subscriber: Subscriber<in Upstream>) {
|
||||||
// initially try only standard upstreams
|
// initially try only standard upstreams
|
||||||
val first = Flux.fromIterable(standardUpstreams)
|
val first = Flux.fromIterable(primaryUpstreams)
|
||||||
|
val second = Flux.fromIterable(secondaryUpstreams)
|
||||||
// if all failed, try both standard and fallback upstreams, repeating in cycle
|
// if all failed, try both standard and fallback upstreams, repeating in cycle
|
||||||
val retries = (0 until (retryLimit - 1)).map { r ->
|
val retries = (0 until (retryLimit - 1)).map { r ->
|
||||||
Flux.fromIterable(standardWithFallback)
|
Flux.fromIterable(standardWithFallback)
|
||||||
// add delay to let upstream to restore if it's a temp failure
|
// add a delay to let upstream to restore if it's a temp failure
|
||||||
// but delay only start of the check, not between upstreams
|
// but delay only start of the check, not between upstreams
|
||||||
// i.e. if all upstreams failed -> wait -> check all without waiting in between
|
// i.e. if all upstreams failed -> wait -> check all without waiting in between
|
||||||
.delaySubscription(waitDuration(r + 1))
|
.delaySubscription(waitDuration(r + 1))
|
||||||
}.let { Flux.concat(it) }
|
}.let { Flux.concat(it) }
|
||||||
|
|
||||||
var result = Flux.concat(first, retries)
|
var result = Flux.concat(first, second, retries)
|
||||||
|
|
||||||
if (Global.metricsExtended) {
|
if (Global.metricsExtended) {
|
||||||
var count = 0
|
var count = 0
|
||||||
@@ -186,9 +195,13 @@ class FilteredApis(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Monitoring(chain: Chain) {
|
class Monitoring(chain: Chain) {
|
||||||
val countStd: DistributionSummary = DistributionSummary.builder("$metricsCode.exist")
|
val countPrimary: DistributionSummary = DistributionSummary.builder("$metricsCode.exist")
|
||||||
.description("Count of available upstreams to select")
|
.description("Count of available upstreams to select")
|
||||||
.tags(listOf(Tag.of("chain", chain.chainCode), Tag.of("role", "std")))
|
.tags(listOf(Tag.of("chain", chain.chainCode), Tag.of("role", "primary")))
|
||||||
|
.register(Metrics.globalRegistry)
|
||||||
|
val countSecondary: DistributionSummary = DistributionSummary.builder("$metricsCode.exist")
|
||||||
|
.description("Count of available upstreams to select")
|
||||||
|
.tags(listOf(Tag.of("chain", chain.chainCode), Tag.of("role", "secondary")))
|
||||||
.register(Metrics.globalRegistry)
|
.register(Metrics.globalRegistry)
|
||||||
val countFallback: DistributionSummary = DistributionSummary.builder("$metricsCode.exist")
|
val countFallback: DistributionSummary = DistributionSummary.builder("$metricsCode.exist")
|
||||||
.description("Count of available fallback upstreams to select")
|
.description("Count of available fallback upstreams to select")
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ abstract class Multistream(
|
|||||||
|
|
||||||
// TODO roles for multistream are useless
|
// TODO roles for multistream are useless
|
||||||
override fun getRole(): UpstreamsConfig.UpstreamRole {
|
override fun getRole(): UpstreamsConfig.UpstreamRole {
|
||||||
return UpstreamsConfig.UpstreamRole.STANDARD
|
return UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMethods(): CallMethods {
|
override fun getMethods(): CallMethods {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ open class EthereumRpcUpstream(
|
|||||||
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>) :
|
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>) :
|
||||||
this(
|
this(
|
||||||
id, chain, api, null,
|
id, chain, api, null,
|
||||||
UpstreamsConfig.Options.getDefaults(), UpstreamsConfig.UpstreamRole.STANDARD,
|
UpstreamsConfig.Options.getDefaults(), UpstreamsConfig.UpstreamRole.PRIMARY,
|
||||||
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||||
DirectCallMethods()
|
DirectCallMethods()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import java.util.function.Function
|
|||||||
|
|
||||||
class BitcoinGrpcUpstream(
|
class BitcoinGrpcUpstream(
|
||||||
private val parentId: String,
|
private val parentId: String,
|
||||||
|
role: UpstreamsConfig.UpstreamRole,
|
||||||
chain: Chain,
|
chain: Chain,
|
||||||
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val client: JsonRpcGrpcClient
|
private val client: JsonRpcGrpcClient
|
||||||
@@ -52,7 +53,7 @@ class BitcoinGrpcUpstream(
|
|||||||
"$parentId/${chain.chainCode}",
|
"$parentId/${chain.chainCode}",
|
||||||
chain,
|
chain,
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
UpstreamsConfig.UpstreamRole.STANDARD
|
role
|
||||||
),
|
),
|
||||||
GrpcUpstream,
|
GrpcUpstream,
|
||||||
Lifecycle {
|
Lifecycle {
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ import java.util.function.Function
|
|||||||
|
|
||||||
open class EthereumGrpcUpstream(
|
open class EthereumGrpcUpstream(
|
||||||
private val parentId: String,
|
private val parentId: String,
|
||||||
|
role: UpstreamsConfig.UpstreamRole,
|
||||||
private val chain: Chain,
|
private val chain: Chain,
|
||||||
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val client: JsonRpcGrpcClient
|
private val client: JsonRpcGrpcClient
|
||||||
) : EthereumUpstream(
|
) : EthereumUpstream(
|
||||||
"$parentId/${chain.chainCode}",
|
"$parentId/${chain.chainCode}",
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
UpstreamsConfig.UpstreamRole.STANDARD,
|
role,
|
||||||
null, null
|
null, null
|
||||||
),
|
),
|
||||||
GrpcUpstream,
|
GrpcUpstream,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
|||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
@@ -51,6 +52,7 @@ import kotlin.concurrent.withLock
|
|||||||
|
|
||||||
class GrpcUpstreams(
|
class GrpcUpstreams(
|
||||||
private val id: String,
|
private val id: String,
|
||||||
|
private val role: UpstreamsConfig.UpstreamRole,
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
private val auth: AuthConfig.ClientTlsAuth? = null,
|
private val auth: AuthConfig.ClientTlsAuth? = null,
|
||||||
@@ -198,7 +200,7 @@ class GrpcUpstreams(
|
|||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
val rpcClient = JsonRpcGrpcClient(client!!, chain, metrics)
|
val rpcClient = JsonRpcGrpcClient(client!!, chain, metrics)
|
||||||
val created = EthereumGrpcUpstream(id, chain, client!!, rpcClient)
|
val created = EthereumGrpcUpstream(id, role, chain, client!!, rpcClient)
|
||||||
created.timeout = this.timeout
|
created.timeout = this.timeout
|
||||||
known[chain] = created
|
known[chain] = created
|
||||||
created.start()
|
created.start()
|
||||||
@@ -214,7 +216,7 @@ class GrpcUpstreams(
|
|||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
val rpcClient = JsonRpcGrpcClient(client!!, chain, metrics)
|
val rpcClient = JsonRpcGrpcClient(client!!, chain, metrics)
|
||||||
val created = BitcoinGrpcUpstream(id, chain, client!!, rpcClient)
|
val created = BitcoinGrpcUpstream(id, role, chain, client!!, rpcClient)
|
||||||
created.timeout = this.timeout
|
created.timeout = this.timeout
|
||||||
known[chain] = created
|
known[chain] = created
|
||||||
created.start()
|
created.start()
|
||||||
|
|||||||
@@ -343,8 +343,8 @@ class UpstreamsConfigReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
act.upstreams.size() == 2
|
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.STANDARD
|
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Parse config with fallback role"() {
|
def "Parse config with fallback role"() {
|
||||||
@@ -355,10 +355,23 @@ class UpstreamsConfigReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
act.upstreams.size() == 2
|
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
|
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"() {
|
def "Parse config with invalid role"() {
|
||||||
setup:
|
setup:
|
||||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-invalid.yaml")
|
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-invalid.yaml")
|
||||||
@@ -367,7 +380,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
act.upstreams.size() == 2
|
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.STANDARD
|
act.upstreams.get(1).role == UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
1 * getApi() >> Mock(Reader) {
|
1 * getApi() >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1"))
|
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1"))
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
}
|
}
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> api
|
_ * getApi() >> api
|
||||||
}
|
}
|
||||||
def apis = new FilteredApis(
|
def apis = new FilteredApis(
|
||||||
@@ -98,7 +98,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> Mock(Reader) {
|
_ * getApi() >> Mock(Reader) {
|
||||||
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
Mono.just(JsonRpcResponse.ok("null")),
|
Mono.just(JsonRpcResponse.ok("null")),
|
||||||
@@ -130,7 +130,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> Mock(Reader) {
|
_ * getApi() >> Mock(Reader) {
|
||||||
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
Mono.just(JsonRpcResponse.error(1, "test")),
|
Mono.just(JsonRpcResponse.error(1, "test")),
|
||||||
@@ -161,7 +161,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> Mock(Reader) {
|
_ * getApi() >> Mock(Reader) {
|
||||||
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
Mono.just(JsonRpcResponse.ok("null")),
|
Mono.just(JsonRpcResponse.ok("null")),
|
||||||
@@ -200,7 +200,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
}
|
}
|
||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> api
|
_ * getApi() >> api
|
||||||
}
|
}
|
||||||
def apis = new FilteredApis(
|
def apis = new FilteredApis(
|
||||||
@@ -226,7 +226,7 @@ class QuorumRpcReaderSpec extends Specification {
|
|||||||
def up = Mock(Upstream) {
|
def up = Mock(Upstream) {
|
||||||
_ * getLag() >> 0
|
_ * getLag() >> 0
|
||||||
_ * isAvailable() >> true
|
_ * isAvailable() >> true
|
||||||
_ * getRole() >> UpstreamsConfig.UpstreamRole.STANDARD
|
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
_ * getApi() >> Mock(Reader) {
|
_ * getApi() >> Mock(Reader) {
|
||||||
_ * read(new JsonRpcRequest("eth_test", [])) >>> [
|
_ * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
Mono.just(JsonRpcResponse.error(-3010, "test")),
|
Mono.just(JsonRpcResponse.error(-3010, "test")),
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class EthereumUpstreamMock extends EthereumRpcUpstream {
|
|||||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
|
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
|
||||||
super(id, chain, api, null,
|
super(id, chain, api, null,
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
UpstreamsConfig.UpstreamRole.STANDARD,
|
UpstreamsConfig.UpstreamRole.PRIMARY,
|
||||||
new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
|
new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
|
||||||
methods)
|
methods)
|
||||||
setLag(0)
|
setLag(0)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
TestingCommons.api().tap { it.id = "${i++}" },
|
TestingCommons.api().tap { it.id = "${i++}" },
|
||||||
(EthereumWsFactory) null,
|
(EthereumWsFactory) null,
|
||||||
new UpstreamsConfig.Options(),
|
new UpstreamsConfig.Options(),
|
||||||
UpstreamsConfig.UpstreamRole.STANDARD,
|
UpstreamsConfig.UpstreamRole.PRIMARY,
|
||||||
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)),
|
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)),
|
||||||
ethereumTargets
|
ethereumTargets
|
||||||
)
|
)
|
||||||
@@ -210,7 +210,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
6 | [0, 1]
|
6 | [0, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Starts with standard"() {
|
def "Starts with primary"() {
|
||||||
setup:
|
setup:
|
||||||
List<Upstream> standard = (0..1).collect {
|
List<Upstream> standard = (0..1).collect {
|
||||||
TestingCommons.upstream(
|
TestingCommons.upstream(
|
||||||
@@ -241,4 +241,45 @@ class FilteredApisSpec extends Specification {
|
|||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify(Duration.ofSeconds(1))
|
.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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import io.emeraldpay.api.proto.BlockchainGrpc
|
|||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.test.MockGrpcServer
|
import io.emeraldpay.dshackle.test.MockGrpcServer
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
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.setLag(0)
|
||||||
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
.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.setLag(0)
|
||||||
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
||||||
@@ -200,7 +201,7 @@ class EthereumGrpcUpstreamSpec extends Specification {
|
|||||||
finished.complete(true)
|
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.setLag(0)
|
||||||
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
||||||
|
|||||||
44
src/test/resources/upstreams-roles-2.yaml
Normal file
44
src/test/resources/upstreams-roles-2.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user