@@ -74,6 +74,7 @@ class UpstreamsConfig {
|
||||
var connection: T? = null
|
||||
val labels = Labels()
|
||||
var methods: Methods? = null
|
||||
var role: UpstreamRole = UpstreamRole.STANDARD
|
||||
|
||||
@Suppress("unchecked")
|
||||
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
||||
@@ -84,6 +85,11 @@ class UpstreamsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
enum class UpstreamRole {
|
||||
STANDARD,
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
open class UpstreamConnection
|
||||
|
||||
open class RpcConnection : UpstreamConnection() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import reactor.util.function.Tuples
|
||||
import java.io.InputStream
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
|
||||
@@ -185,6 +186,15 @@ class UpstreamsConfigReader(
|
||||
|
||||
internal fun readUpstreamStandard(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) {
|
||||
upstream.chain = getValueAsString(upNode, "chain")
|
||||
getValueAsString(upNode, "role")?.let {
|
||||
val name = it.trim()
|
||||
try {
|
||||
val role = UpstreamsConfig.UpstreamRole.valueOf(name.toUpperCase())
|
||||
upstream.role = role
|
||||
} catch (e: IllegalArgumentException) {
|
||||
log.warn("Unsupported role `$name` for upstream ${upstream.id}")
|
||||
}
|
||||
}
|
||||
if (hasAny(upNode, "labels")) {
|
||||
getMapping(upNode, "labels")?.let { labels ->
|
||||
labels.value.stream()
|
||||
|
||||
@@ -145,7 +145,8 @@ open class ConfiguredUpstreams(
|
||||
val methods = buildMethods(config, chain)
|
||||
val upstream = BitcoinUpstream(config.id
|
||||
?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
|
||||
options, QuorumForLabels.QuorumItem(1, config.labels),
|
||||
options, config.role,
|
||||
QuorumForLabels.QuorumItem(1, config.labels),
|
||||
methods)
|
||||
|
||||
upstream.start()
|
||||
@@ -183,7 +184,8 @@ open class ConfiguredUpstreams(
|
||||
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
||||
val ethereumUpstream = EthereumUpstream(
|
||||
config.id!!,
|
||||
chain, directApi, wsFactoryApi, options,
|
||||
chain, directApi, wsFactoryApi,
|
||||
options, config.role,
|
||||
QuorumForLabels.QuorumItem(1, config.labels),
|
||||
methods
|
||||
)
|
||||
|
||||
@@ -27,10 +27,12 @@ abstract class DefaultUpstream(
|
||||
defaultLag: Long,
|
||||
defaultAvail: UpstreamAvailability,
|
||||
private val options: UpstreamsConfig.Options,
|
||||
private val role: UpstreamsConfig.UpstreamRole,
|
||||
private val targets: CallMethods?
|
||||
) : Upstream {
|
||||
|
||||
constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets)
|
||||
constructor(id: String, options: UpstreamsConfig.Options, role: UpstreamsConfig.UpstreamRole, targets: CallMethods?) :
|
||||
this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, role, targets)
|
||||
|
||||
private val status = AtomicReference(Status(defaultLag, defaultAvail, statusByLag(defaultLag, defaultAvail)))
|
||||
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||
@@ -85,6 +87,10 @@ abstract class DefaultUpstream(
|
||||
return options
|
||||
}
|
||||
|
||||
override fun getRole(): UpstreamsConfig.UpstreamRole {
|
||||
return role;
|
||||
}
|
||||
|
||||
override fun getMethods(): CallMethods {
|
||||
return targets ?: throw IllegalStateException("Methods are not set")
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import org.reactivestreams.Subscriber
|
||||
import reactor.core.publisher.EmitterProcessor
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -33,13 +31,26 @@ class FilteredApis(
|
||||
allUpstreams: List<Upstream>,
|
||||
private val matcher: Selector.Matcher,
|
||||
pos: Int,
|
||||
private val repeatLimit: Long,
|
||||
/**
|
||||
* Limit of retries
|
||||
*/
|
||||
private val retryLimit: Long,
|
||||
jitter: Int
|
||||
) : ApiSource {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_DELAY_STEP = 100
|
||||
private const val MAX_WAIT_MILLIS = 5000L
|
||||
|
||||
@JvmStatic
|
||||
fun <T> startFrom(upstreams: List<T>, pos: Int): List<T> {
|
||||
return if (upstreams.size <= 1 || pos == 0) {
|
||||
upstreams
|
||||
} else {
|
||||
val safePosition = pos % upstreams.size
|
||||
upstreams.subList(safePosition, upstreams.size) + upstreams.subList(0, safePosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(allUpstreams: List<Upstream>,
|
||||
@@ -50,7 +61,8 @@ class FilteredApis(
|
||||
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
|
||||
|
||||
private val delay: Int
|
||||
private val upstreams: List<Upstream>
|
||||
private val standardUpstreams: List<Upstream>
|
||||
private val standardWithFallback: List<Upstream>
|
||||
|
||||
private val control = EmitterProcessor.create<Boolean>(32, false)
|
||||
|
||||
@@ -61,12 +73,20 @@ class FilteredApis(
|
||||
DEFAULT_DELAY_STEP
|
||||
}
|
||||
|
||||
upstreams = if (allUpstreams.size == 1 || pos == 0 || allUpstreams.isEmpty()) {
|
||||
allUpstreams
|
||||
} else {
|
||||
val safePosition = pos % allUpstreams.size
|
||||
allUpstreams.subList(safePosition, allUpstreams.size) + allUpstreams.subList(0, safePosition)
|
||||
standardUpstreams = allUpstreams.filter {
|
||||
it.getRole() == UpstreamsConfig.UpstreamRole.STANDARD
|
||||
}.let {
|
||||
startFrom(it, pos)
|
||||
}
|
||||
val fallbackUpstreams = allUpstreams.filter {
|
||||
it.getRole() == UpstreamsConfig.UpstreamRole.FALLBACK
|
||||
}.let {
|
||||
startFrom(it, pos)
|
||||
}
|
||||
standardWithFallback = emptyList<Upstream>()
|
||||
.plus(standardUpstreams)
|
||||
.plus(fallbackUpstreams)
|
||||
|
||||
}
|
||||
|
||||
fun waitDuration(rawn: Long): Duration {
|
||||
@@ -79,9 +99,15 @@ class FilteredApis(
|
||||
}
|
||||
|
||||
override fun subscribe(subscriber: Subscriber<in Upstream>) {
|
||||
val first = Flux.fromIterable(upstreams)
|
||||
val retries = (1 until repeatLimit).map { r ->
|
||||
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
||||
// initially try only standard upstreams
|
||||
val first = Flux.fromIterable(standardUpstreams)
|
||||
// if all failed, try both standard and fallback upstreams, repeating in cycle
|
||||
val retries = (0 until (retryLimit - 1)).map { r ->
|
||||
Flux.fromIterable(standardWithFallback)
|
||||
// add delay to let upstream to restore if it's a temp failure
|
||||
// but delay only start of the check, not between upstreams
|
||||
// i.e. if all upstreams failed -> wait -> check all without waiting in between
|
||||
.delaySubscription(waitDuration(r + 1))
|
||||
}.let { Flux.concat(it) }
|
||||
|
||||
Flux.concat(first, retries)
|
||||
|
||||
@@ -145,10 +145,16 @@ abstract class Multistream(
|
||||
else upstreams.map { it.getStatus() }.min()!!
|
||||
}
|
||||
|
||||
//TODO options for multistream are useless
|
||||
override fun getOptions(): UpstreamsConfig.Options {
|
||||
return UpstreamsConfig.Options()
|
||||
}
|
||||
|
||||
//TODO roles for multistream are useless
|
||||
override fun getRole(): UpstreamsConfig.UpstreamRole {
|
||||
return UpstreamsConfig.UpstreamRole.STANDARD
|
||||
}
|
||||
|
||||
override fun getMethods(): CallMethods {
|
||||
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ interface Upstream {
|
||||
fun getHead(): Head
|
||||
fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse>
|
||||
fun getOptions(): UpstreamsConfig.Options
|
||||
fun getRole(): UpstreamsConfig.UpstreamRole
|
||||
fun setLag(lag: Long)
|
||||
fun getLag(): Long
|
||||
fun getLabels(): Collection<UpstreamsConfig.Labels>
|
||||
|
||||
@@ -34,9 +34,10 @@ open class BitcoinUpstream(
|
||||
val chain: Chain,
|
||||
private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
options: UpstreamsConfig.Options,
|
||||
role: UpstreamsConfig.UpstreamRole,
|
||||
val node: QuorumForLabels.QuorumItem,
|
||||
callMethods: CallMethods
|
||||
) : DefaultUpstream(id, options, callMethods), Lifecycle {
|
||||
) : DefaultUpstream(id, options, role, callMethods), Lifecycle {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
||||
|
||||
@@ -40,13 +40,16 @@ open class EthereumUpstream(
|
||||
private val directReader: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
private val ethereumWsFactory: EthereumWsFactory? = null,
|
||||
options: UpstreamsConfig.Options,
|
||||
role: UpstreamsConfig.UpstreamRole,
|
||||
val node: QuorumForLabels.QuorumItem,
|
||||
targets: CallMethods
|
||||
) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle {
|
||||
) : DefaultUpstream(id, options, role, targets), Upstream, CachesEnabled, Lifecycle {
|
||||
|
||||
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>) : this(id, chain, api, null,
|
||||
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||
DirectCallMethods())
|
||||
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>) :
|
||||
this(id, chain, api, null,
|
||||
UpstreamsConfig.Options.getDefaults(), UpstreamsConfig.UpstreamRole.STANDARD,
|
||||
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||
DirectCallMethods())
|
||||
|
||||
|
||||
private val log = LoggerFactory.getLogger(EthereumUpstream::class.java)
|
||||
|
||||
@@ -60,6 +60,7 @@ open class EthereumGrpcUpstream(
|
||||
) : DefaultUpstream(
|
||||
"$parentId/${chain.chainCode}",
|
||||
UpstreamsConfig.Options.getDefaults(),
|
||||
UpstreamsConfig.UpstreamRole.STANDARD,
|
||||
null
|
||||
), Lifecycle {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user