Reanimate grpc upstreams (#491)
This commit is contained in:
@@ -10,6 +10,7 @@ import org.springframework.context.annotation.Configuration
|
|||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -63,6 +64,11 @@ open class SchedulersConfig {
|
|||||||
return makeScheduler("head-liveness-scheduler", 4, monitoringConfig)
|
return makeScheduler("head-liveness-scheduler", 4, monitoringConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun grpcChannelExecutor(monitoringConfig: MonitoringConfig): Executor {
|
||||||
|
return makePool("grpc-client-channel", 10, monitoringConfig)
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun authScheduler(monitoringConfig: MonitoringConfig): Scheduler {
|
open fun authScheduler(monitoringConfig: MonitoringConfig): Scheduler {
|
||||||
return makeScheduler("auth-scheduler", 4, monitoringConfig)
|
return makeScheduler("auth-scheduler", 4, monitoringConfig)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.startup
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||||
import io.emeraldpay.dshackle.startup.configure.UpstreamCreationData
|
import io.emeraldpay.dshackle.startup.configure.UpstreamCreationData
|
||||||
@@ -33,6 +34,7 @@ open class ConfiguredUpstreams(
|
|||||||
private val upstreamFactory: UpstreamFactory,
|
private val upstreamFactory: UpstreamFactory,
|
||||||
private val config: UpstreamsConfig,
|
private val config: UpstreamsConfig,
|
||||||
private val multistreamHolder: CurrentMultistreamHolder,
|
private val multistreamHolder: CurrentMultistreamHolder,
|
||||||
|
private val chainsConfig: ChainsConfig,
|
||||||
) : ApplicationRunner {
|
) : ApplicationRunner {
|
||||||
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
||||||
|
|
||||||
@@ -71,6 +73,19 @@ open class ConfiguredUpstreams(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
upstreamFactory.createGrpcUpstream(
|
||||||
|
up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>,
|
||||||
|
chainsConfig,
|
||||||
|
)
|
||||||
|
.start()
|
||||||
|
.doOnNext {
|
||||||
|
log.info("Chain ${it.chain} ${it.type} through gRPC at ${up.connection?.host}:${up.connection?.port}. With caps: ${it.upstream.getCapabilities()}")
|
||||||
|
}
|
||||||
|
.subscribe {
|
||||||
|
multistreamHolder.getUpstream(it.chain)
|
||||||
|
.processUpstreamsEvents(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry
|
|||||||
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
|
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory
|
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.util.function.Function
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
open class GenericUpstreamCreator(
|
open class GenericUpstreamCreator(
|
||||||
@@ -72,7 +70,7 @@ open class GenericUpstreamCreator(
|
|||||||
val hashUrl = connection.let {
|
val hashUrl = connection.let {
|
||||||
if (it.connectorMode == GenericConnectorFactory.ConnectorMode.RPC_REQUESTS_WITH_MIXED_HEAD.name) it.rpc?.url ?: it.ws?.url else it.ws?.url ?: it.rpc?.url
|
if (it.connectorMode == GenericConnectorFactory.ConnectorMode.RPC_REQUESTS_WITH_MIXED_HEAD.name) it.rpc?.url ?: it.ws?.url else it.ws?.url ?: it.rpc?.url
|
||||||
}
|
}
|
||||||
val hash = getHash(nodeId, hashUrl!!)
|
val hash = getHash(nodeId, hashUrl!!, hashes)
|
||||||
|
|
||||||
val upstream = GenericUpstream(
|
val upstream = GenericUpstream(
|
||||||
config.id!!,
|
config.id!!,
|
||||||
@@ -96,25 +94,4 @@ open class GenericUpstreamCreator(
|
|||||||
}
|
}
|
||||||
return UpstreamCreationData(upstream, upstream.isValid())
|
return UpstreamCreationData(upstream, upstream.isValid())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHash(nodeId: Int?, obj: Any): Byte =
|
|
||||||
nodeId?.toByte() ?: (obj.hashCode() % 255).let {
|
|
||||||
if (it == 0) 1 else it
|
|
||||||
}.let { nonZeroHash ->
|
|
||||||
listOf<Function<Int, Int>>(
|
|
||||||
Function { i -> i },
|
|
||||||
Function { i -> (-i) },
|
|
||||||
Function { i -> 127 - abs(i) },
|
|
||||||
Function { i -> abs(i) - 128 },
|
|
||||||
).map {
|
|
||||||
it.apply(nonZeroHash).toByte()
|
|
||||||
}.firstOrNull {
|
|
||||||
hashes[it] != true
|
|
||||||
}?.let {
|
|
||||||
hashes[it] = true
|
|
||||||
it
|
|
||||||
} ?: (Byte.MIN_VALUE..Byte.MAX_VALUE).first {
|
|
||||||
it != 0 && hashes[it.toByte()] != true
|
|
||||||
}.toByte()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.function.Function
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
abstract class UpstreamCreator(
|
abstract class UpstreamCreator(
|
||||||
private val chainsConfig: ChainsConfig,
|
private val chainsConfig: ChainsConfig,
|
||||||
@@ -19,6 +21,29 @@ abstract class UpstreamCreator(
|
|||||||
) {
|
) {
|
||||||
protected val log: Logger = LoggerFactory.getLogger(this::class.java)
|
protected val log: Logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getHash(nodeId: Int?, obj: Any, hashes: MutableMap<Byte, Boolean>): Byte =
|
||||||
|
nodeId?.toByte() ?: (obj.hashCode() % 255).let {
|
||||||
|
if (it == 0) 1 else it
|
||||||
|
}.let { nonZeroHash ->
|
||||||
|
listOf<Function<Int, Int>>(
|
||||||
|
Function { i -> i },
|
||||||
|
Function { i -> (-i) },
|
||||||
|
Function { i -> 127 - abs(i) },
|
||||||
|
Function { i -> abs(i) - 128 },
|
||||||
|
).map {
|
||||||
|
it.apply(nonZeroHash).toByte()
|
||||||
|
}.firstOrNull {
|
||||||
|
hashes[it] != true
|
||||||
|
}?.let {
|
||||||
|
hashes[it] = true
|
||||||
|
it
|
||||||
|
} ?: (Byte.MIN_VALUE..Byte.MAX_VALUE).first {
|
||||||
|
it != 0 && hashes[it.toByte()] != true
|
||||||
|
}.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createUpstream(
|
fun createUpstream(
|
||||||
upstreamsConfig: UpstreamsConfig.Upstream<*>,
|
upstreamsConfig: UpstreamsConfig.Upstream<*>,
|
||||||
defaultOptions: Map<Chain, ChainOptions.PartialOptions>,
|
defaultOptions: Map<Chain, ChainOptions.PartialOptions>,
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package io.emeraldpay.dshackle.startup.configure
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreamCreator
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
data class UpstreamCreationData(
|
data class UpstreamCreationData(
|
||||||
@@ -21,6 +24,7 @@ class UpstreamFactory(
|
|||||||
private val genericUpstreamCreator: GenericUpstreamCreator,
|
private val genericUpstreamCreator: GenericUpstreamCreator,
|
||||||
private val ethereumUpstreamCreator: EthereumUpstreamCreator,
|
private val ethereumUpstreamCreator: EthereumUpstreamCreator,
|
||||||
private val bitcoinUpstreamCreator: BitcoinUpstreamCreator,
|
private val bitcoinUpstreamCreator: BitcoinUpstreamCreator,
|
||||||
|
private val grpcUpstreamCreator: GrpcUpstreamCreator,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createUpstream(
|
fun createUpstream(
|
||||||
@@ -34,4 +38,11 @@ class UpstreamFactory(
|
|||||||
else -> genericUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions)
|
else -> genericUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createGrpcUpstream(
|
||||||
|
config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>,
|
||||||
|
chainsConfig: ChainsConfig,
|
||||||
|
): GrpcUpstreams {
|
||||||
|
return grpcUpstreamCreator.creatGrpcUpstream(config, chainsConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||||
@@ -39,6 +40,7 @@ abstract class DefaultUpstream(
|
|||||||
private val targets: CallMethods?,
|
private val targets: CallMethods?,
|
||||||
private val node: QuorumForLabels.QuorumItem?,
|
private val node: QuorumForLabels.QuorumItem?,
|
||||||
private val chainConfig: ChainsConfig.ChainConfig,
|
private val chainConfig: ChainsConfig.ChainConfig,
|
||||||
|
private val chain: Chain,
|
||||||
) : Upstream {
|
) : Upstream {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -49,8 +51,9 @@ abstract class DefaultUpstream(
|
|||||||
targets: CallMethods?,
|
targets: CallMethods?,
|
||||||
node: QuorumForLabels.QuorumItem?,
|
node: QuorumForLabels.QuorumItem?,
|
||||||
chainConfig: ChainsConfig.ChainConfig,
|
chainConfig: ChainsConfig.ChainConfig,
|
||||||
|
chain: Chain,
|
||||||
) :
|
) :
|
||||||
this(id, hash, null, UpstreamAvailability.UNAVAILABLE, options, role, targets, node, chainConfig)
|
this(id, hash, null, UpstreamAvailability.UNAVAILABLE, options, role, targets, node, chainConfig, chain)
|
||||||
|
|
||||||
protected val log = LoggerFactory.getLogger(this::class.java)
|
protected val log = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
@@ -159,5 +162,11 @@ abstract class DefaultUpstream(
|
|||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun sendUpstreamStateEvent(eventType: UpstreamChangeEvent.ChangeType) {
|
||||||
|
stateEventStream.emitNext(
|
||||||
|
UpstreamChangeEvent(chain, this, eventType),
|
||||||
|
) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||||
|
}
|
||||||
|
|
||||||
data class Status(val lag: Long?, val avail: UpstreamAvailability, val status: UpstreamAvailability)
|
data class Status(val lag: Long?, val avail: UpstreamAvailability, val status: UpstreamAvailability)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse
|
|||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
|
||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse
|
||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.fromProtoType
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class Selector {
|
|||||||
} else if (selector.hasLowerHeightSelector()) {
|
} else if (selector.hasLowerHeightSelector()) {
|
||||||
return Sort(
|
return Sort(
|
||||||
compareBy(nullsLast()) {
|
compareBy(nullsLast()) {
|
||||||
it.getLowerBound(fromProtoType(selector.lowerHeightSelector.lowerBoundType))?.lowerBound
|
it.getLowerBound(selector.lowerHeightSelector.lowerBoundType.fromProtoType())?.lowerBound
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,16 +84,6 @@ class Selector {
|
|||||||
return Sort.default
|
return Sort.default
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fromProtoType(type: BlockchainOuterClass.LowerBoundType): LowerBoundType {
|
|
||||||
return when (type) {
|
|
||||||
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_SLOT -> LowerBoundType.SLOT
|
|
||||||
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_UNSPECIFIED -> LowerBoundType.UNKNOWN
|
|
||||||
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_STATE -> LowerBoundType.STATE
|
|
||||||
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_BLOCK -> LowerBoundType.BLOCK
|
|
||||||
BlockchainOuterClass.LowerBoundType.UNRECOGNIZED -> LowerBoundType.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun convertToMatcher(req: BlockchainOuterClass.Selector?): LabelSelectorMatcher {
|
fun convertToMatcher(req: BlockchainOuterClass.Selector?): LabelSelectorMatcher {
|
||||||
return when {
|
return when {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ abstract class BitcoinUpstream(
|
|||||||
node: QuorumForLabels.QuorumItem,
|
node: QuorumForLabels.QuorumItem,
|
||||||
val esploraClient: EsploraClient? = null,
|
val esploraClient: EsploraClient? = null,
|
||||||
chainConfig: ChainsConfig.ChainConfig,
|
chainConfig: ChainsConfig.ChainConfig,
|
||||||
) : DefaultUpstream(id, 0.toByte(), options, role, callMethods, node, chainConfig) {
|
) : DefaultUpstream(id, 0.toByte(), options, role, callMethods, node, chainConfig, chain) {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ open class GenericMultistream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addUpstreamInternal(u: Upstream) {
|
override fun addUpstreamInternal(u: Upstream) {
|
||||||
upstreams.add(u as GenericUpstream)
|
upstreams.add(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val head: DynamicMergedHead = DynamicMergedHead(
|
private val head: DynamicMergedHead = DynamicMergedHead(
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
|||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
@@ -47,7 +46,7 @@ open class GenericUpstream(
|
|||||||
validatorBuilder: UpstreamValidatorBuilder,
|
validatorBuilder: UpstreamValidatorBuilder,
|
||||||
upstreamSettingsDetectorBuilder: UpstreamSettingsDetectorBuilder,
|
upstreamSettingsDetectorBuilder: UpstreamSettingsDetectorBuilder,
|
||||||
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
||||||
) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig), Lifecycle {
|
) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig, chain), Lifecycle {
|
||||||
|
|
||||||
private val validator: UpstreamValidator? = validatorBuilder(chain, this, getOptions(), chainConfig)
|
private val validator: UpstreamValidator? = validatorBuilder(chain, this, getOptions(), chainConfig)
|
||||||
private var validatorSubscription: Disposable? = null
|
private var validatorSubscription: Disposable? = null
|
||||||
@@ -255,10 +254,4 @@ open class GenericUpstream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isValid(): Boolean = isUpstreamValid.get()
|
fun isValid(): Boolean = isUpstreamValid.get()
|
||||||
|
|
||||||
private fun sendUpstreamStateEvent(eventType: UpstreamChangeEvent.ChangeType) {
|
|
||||||
stateEventStream.emitNext(
|
|
||||||
UpstreamChangeEvent(chain, this, eventType),
|
|
||||||
) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class BitcoinGrpcUpstream(
|
|||||||
|
|
||||||
private val extractBlock = ExtractBlock()
|
private val extractBlock = ExtractBlock()
|
||||||
private val defaultReader: ChainReader = client.getReader()
|
private val defaultReader: ChainReader = client.getReader()
|
||||||
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
|
private val blockConverter: Function<BlockchainOuterClass.ChainHead, GrpcHead.GrpcHeadData> = Function { value ->
|
||||||
val parentHash =
|
val parentHash =
|
||||||
if (value.parentBlockId.isBlank()) {
|
if (value.parentBlockId.isBlank()) {
|
||||||
null
|
null
|
||||||
@@ -89,7 +89,7 @@ class BitcoinGrpcUpstream(
|
|||||||
null,
|
null,
|
||||||
parentHash,
|
parentHash,
|
||||||
)
|
)
|
||||||
block
|
GrpcHead.GrpcHeadData(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val reloadBlock: Function<BlockContainer, Publisher<BlockContainer>> = Function { existingBlock ->
|
private val reloadBlock: Function<BlockContainer, Publisher<BlockContainer>> = Function { existingBlock ->
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import io.emeraldpay.dshackle.data.BlockId
|
|||||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||||
import io.emeraldpay.dshackle.reader.ChainReader
|
import io.emeraldpay.dshackle.reader.ChainReader
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
|
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
||||||
import io.emeraldpay.dshackle.upstream.BuildInfo
|
import io.emeraldpay.dshackle.upstream.BuildInfo
|
||||||
import io.emeraldpay.dshackle.upstream.Capability
|
import io.emeraldpay.dshackle.upstream.Capability
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
@@ -37,12 +38,17 @@ import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
|
|||||||
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
|
import io.emeraldpay.dshackle.upstream.lowerbound.fromProtoType
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
|
import reactor.core.scheduler.Schedulers
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
open class GenericGrpcUpstream(
|
open class GenericGrpcUpstream(
|
||||||
@@ -64,11 +70,12 @@ open class GenericGrpcUpstream(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
chainConfig,
|
chainConfig,
|
||||||
|
chain,
|
||||||
),
|
),
|
||||||
GrpcUpstream,
|
GrpcUpstream,
|
||||||
Lifecycle {
|
Lifecycle {
|
||||||
|
|
||||||
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
|
private val blockConverter: Function<BlockchainOuterClass.ChainHead, GrpcHead.GrpcHeadData> = Function { value ->
|
||||||
val parentHash =
|
val parentHash =
|
||||||
if (value.parentBlockId.isBlank()) {
|
if (value.parentBlockId.isBlank()) {
|
||||||
null
|
null
|
||||||
@@ -85,7 +92,9 @@ open class GenericGrpcUpstream(
|
|||||||
null,
|
null,
|
||||||
parentHash,
|
parentHash,
|
||||||
)
|
)
|
||||||
block
|
val lowerBounds = value.lowerBoundsList
|
||||||
|
.map { LowerBoundData(it.lowerBoundValue, it.lowerBoundTimestamp, it.lowerBoundType.fromProtoType()) }
|
||||||
|
GrpcHead.GrpcHeadData(block, lowerBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val upstreamStatus = GrpcUpstreamStatus(overrideLabels)
|
private val upstreamStatus = GrpcUpstreamStatus(overrideLabels)
|
||||||
@@ -104,7 +113,15 @@ open class GenericGrpcUpstream(
|
|||||||
|
|
||||||
private val defaultReader: ChainReader = client.getReader()
|
private val defaultReader: ChainReader = client.getReader()
|
||||||
|
|
||||||
|
private val lowerBounds = ConcurrentHashMap<LowerBoundType, LowerBoundData>()
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
grpcHead.lowerBoundsFlux()
|
||||||
|
.publishOn(lowerBoundScheduler)
|
||||||
|
.subscribe {
|
||||||
|
lowerBounds[it.type] = it
|
||||||
|
sendUpstreamStateEvent(UpstreamChangeEvent.ChangeType.UPDATED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
@@ -181,14 +198,24 @@ open class GenericGrpcUpstream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLowerBounds(): Collection<LowerBoundData> {
|
override fun getLowerBounds(): Collection<LowerBoundData> {
|
||||||
return emptyList()
|
return lowerBounds.values
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData? {
|
override fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData? {
|
||||||
return null
|
return lowerBounds[lowerBoundType]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
||||||
return null
|
return Upstream.UpstreamSettingsData(
|
||||||
|
nodeId(),
|
||||||
|
getId(),
|
||||||
|
"unknown",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val lowerBoundScheduler: Scheduler = Schedulers.fromExecutorService(
|
||||||
|
Executors.newFixedThreadPool(4, CustomizableThreadFactory("grpc-lower-bound-")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
|||||||
import io.emeraldpay.dshackle.upstream.Lifecycle
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||||
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.micrometer.core.instrument.Counter
|
import io.micrometer.core.instrument.Counter
|
||||||
import io.micrometer.core.instrument.Metrics
|
import io.micrometer.core.instrument.Metrics
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.Sinks
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import reactor.kotlin.extra.retry.retryExponentialBackoff
|
import reactor.kotlin.extra.retry.retryExponentialBackoff
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -44,7 +46,7 @@ class GrpcHead(
|
|||||||
/**
|
/**
|
||||||
* Converted from remote head details to the block container, which could be partial at this point
|
* Converted from remote head details to the block container, which could be partial at this point
|
||||||
*/
|
*/
|
||||||
private val converter: Function<BlockchainOuterClass.ChainHead, BlockContainer>,
|
private val converter: Function<BlockchainOuterClass.ChainHead, GrpcHeadData>,
|
||||||
/**
|
/**
|
||||||
* Populate block data with all missing details, of any
|
* Populate block data with all missing details, of any
|
||||||
*/
|
*/
|
||||||
@@ -55,6 +57,8 @@ class GrpcHead(
|
|||||||
|
|
||||||
private var headSubscription: Disposable? = null
|
private var headSubscription: Disposable? = null
|
||||||
|
|
||||||
|
private val lowerBoundsSink = Sinks.many().multicast().directBestEffort<LowerBoundData>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a new head subscription with connection to the remote
|
* Initiate a new head subscription with connection to the remote
|
||||||
*/
|
*/
|
||||||
@@ -89,6 +93,10 @@ class GrpcHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var blocks = heads.map(converter)
|
var blocks = heads.map(converter)
|
||||||
|
.doOnNext {
|
||||||
|
it.lowerBounds.forEach { bound -> lowerBoundsSink.tryEmitNext(bound) }
|
||||||
|
}
|
||||||
|
.map { it.block }
|
||||||
.distinctUntilChanged {
|
.distinctUntilChanged {
|
||||||
it.hash
|
it.hash
|
||||||
}.filter { forkChoice.filter(it) }
|
}.filter { forkChoice.filter(it) }
|
||||||
@@ -125,8 +133,17 @@ class GrpcHead(
|
|||||||
headSubscription?.dispose()
|
headSubscription?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun lowerBoundsFlux(): Flux<LowerBoundData> = lowerBoundsSink.asFlux()
|
||||||
|
|
||||||
val headsCounter = Counter.builder("grpc_head_received")
|
val headsCounter = Counter.builder("grpc_head_received")
|
||||||
.tag("upstream", id)
|
.tag("upstream", id)
|
||||||
.tag("chain", chain.chainCode)
|
.tag("chain", chain.chainCode)
|
||||||
.register(Metrics.globalRegistry)
|
.register(Metrics.globalRegistry)
|
||||||
|
|
||||||
|
data class GrpcHeadData(
|
||||||
|
val block: BlockContainer,
|
||||||
|
val lowerBounds: List<LowerBoundData>,
|
||||||
|
) {
|
||||||
|
constructor(block: BlockContainer) : this(block, emptyList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc
|
||||||
|
|
||||||
|
import brave.grpc.GrpcTracing
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
|
import io.emeraldpay.dshackle.config.CompressionConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.startup.configure.UpstreamCreator.Companion.getHash
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.GrpcAuthContext
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import reactor.core.scheduler.Scheduler
|
||||||
|
import reactor.core.scheduler.Schedulers
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class GrpcUpstreamCreator(
|
||||||
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
|
private val compressionConfig: CompressionConfig,
|
||||||
|
private val fileResolver: FileResolver,
|
||||||
|
@Qualifier("grpcChannelExecutor")
|
||||||
|
private val channelExecutor: Executor,
|
||||||
|
private val grpcTracing: GrpcTracing,
|
||||||
|
@Qualifier("headScheduler")
|
||||||
|
private val headScheduler: Scheduler,
|
||||||
|
private val grpcAuthContext: GrpcAuthContext,
|
||||||
|
) {
|
||||||
|
@Value("\${spring.application.max-metadata-size}")
|
||||||
|
private var maxMetadataSize: Int = Defaults.maxMetadataSize
|
||||||
|
|
||||||
|
private val hashes: MutableMap<Byte, Boolean> = HashMap()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val grpcUpstreamsScheduler: Scheduler = Schedulers.fromExecutorService(
|
||||||
|
Executors.newFixedThreadPool(2),
|
||||||
|
"GrpcUpstreamsStatuses",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun creatGrpcUpstream(
|
||||||
|
config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>,
|
||||||
|
chainsConfig: ChainsConfig,
|
||||||
|
): GrpcUpstreams {
|
||||||
|
val endpoint = config.connection!!
|
||||||
|
return GrpcUpstreams(
|
||||||
|
config.id!!,
|
||||||
|
getHash(config.nodeId, "${endpoint.host}:${endpoint.port}", hashes),
|
||||||
|
config.role,
|
||||||
|
endpoint.host!!,
|
||||||
|
endpoint.port,
|
||||||
|
endpoint.auth,
|
||||||
|
endpoint.tokenAuth,
|
||||||
|
authorizationConfig,
|
||||||
|
compressionConfig.grpc.clientEnabled,
|
||||||
|
fileResolver,
|
||||||
|
endpoint.upstreamRating,
|
||||||
|
config.labels,
|
||||||
|
grpcUpstreamsScheduler,
|
||||||
|
channelExecutor,
|
||||||
|
chainsConfig,
|
||||||
|
grpcTracing,
|
||||||
|
null,
|
||||||
|
maxMetadataSize,
|
||||||
|
headScheduler,
|
||||||
|
grpcAuthContext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.lowerbound
|
package io.emeraldpay.dshackle.upstream.lowerbound
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
data class LowerBoundData(
|
data class LowerBoundData(
|
||||||
@@ -21,3 +22,13 @@ data class LowerBoundData(
|
|||||||
enum class LowerBoundType {
|
enum class LowerBoundType {
|
||||||
UNKNOWN, STATE, SLOT, BLOCK
|
UNKNOWN, STATE, SLOT, BLOCK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BlockchainOuterClass.LowerBoundType.fromProtoType(): LowerBoundType {
|
||||||
|
return when (this) {
|
||||||
|
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_SLOT -> LowerBoundType.SLOT
|
||||||
|
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_UNSPECIFIED -> LowerBoundType.UNKNOWN
|
||||||
|
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_STATE -> LowerBoundType.STATE
|
||||||
|
BlockchainOuterClass.LowerBoundType.LOWER_BOUND_BLOCK -> LowerBoundType.BLOCK
|
||||||
|
BlockchainOuterClass.LowerBoundType.UNRECOGNIZED -> LowerBoundType.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import io.emeraldpay.dshackle.upstream.Upstream
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
||||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
|
import io.emeraldpay.dshackle.upstream.stream.Chunk
|
||||||
import io.grpc.StatusRuntimeException
|
import io.grpc.StatusRuntimeException
|
||||||
import org.apache.commons.lang3.time.StopWatch
|
import org.apache.commons.lang3.time.StopWatch
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -53,6 +54,9 @@ class JsonRpcGrpcClient(
|
|||||||
override fun read(key: ChainRequest): Mono<ChainResponse> {
|
override fun read(key: ChainRequest): Mono<ChainResponse> {
|
||||||
val timer = StopWatch()
|
val timer = StopWatch()
|
||||||
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
|
.setChunkSize(
|
||||||
|
if (key.isStreamed) 500 else 0,
|
||||||
|
)
|
||||||
.setChainValue(chain.id)
|
.setChainValue(chain.id)
|
||||||
|
|
||||||
key.selector?.let { req.selector = it }
|
key.selector?.let { req.selector = it }
|
||||||
@@ -91,9 +95,22 @@ class JsonRpcGrpcClient(
|
|||||||
.doOnNext { timer.start() }
|
.doOnNext { timer.start() }
|
||||||
.flatMap {
|
.flatMap {
|
||||||
stub.nativeCall(req.build())
|
stub.nativeCall(req.build())
|
||||||
|
.switchOnFirst({ first, responseStream ->
|
||||||
|
if (first.get()!!.chunked) {
|
||||||
|
Mono.just(
|
||||||
|
ChainResponse(
|
||||||
|
responseStream.map { Chunk(it.payload.toByteArray(), it.finalChunk) },
|
||||||
|
key.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
responseStream
|
||||||
|
.single()
|
||||||
|
.flatMap(::handleResponse)
|
||||||
|
}
|
||||||
|
}, false,)
|
||||||
.single()
|
.single()
|
||||||
.onErrorResume(::handleError)
|
.onErrorResume(::handleError)
|
||||||
.flatMap(::handleResponse)
|
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
if (timer.isStarted) {
|
if (timer.isStarted) {
|
||||||
@@ -122,7 +139,7 @@ class JsonRpcGrpcClient(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleError(t: Throwable): Mono<BlockchainOuterClass.NativeCallReplyItem> {
|
fun handleError(t: Throwable): Mono<ChainResponse> {
|
||||||
metrics?.fails?.increment()
|
metrics?.fails?.increment()
|
||||||
return when (t) {
|
return when (t) {
|
||||||
is StatusRuntimeException -> Mono.error(
|
is StatusRuntimeException -> Mono.error(
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class GrpcHeadSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def convert = { BlockchainOuterClass.ChainHead head ->
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
TestingCommons.blockForBitcoin(head.height)
|
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
|
||||||
}
|
}
|
||||||
def head = new GrpcHead(
|
def head = new GrpcHead(
|
||||||
"test",
|
"test",
|
||||||
@@ -127,7 +127,7 @@ class GrpcHeadSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def convert = { BlockchainOuterClass.ChainHead head ->
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
TestingCommons.blockForBitcoin(head.height)
|
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
|
||||||
}
|
}
|
||||||
def head = new GrpcHead(
|
def head = new GrpcHead(
|
||||||
"test",
|
"test",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.startup.configure
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreamCreator
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
@@ -33,7 +34,13 @@ class UpstreamFactoryTest {
|
|||||||
private val genericUpstreamCreator = mock<GenericUpstreamCreator>()
|
private val genericUpstreamCreator = mock<GenericUpstreamCreator>()
|
||||||
private val ethereumUpstreamCreator = mock<EthereumUpstreamCreator>()
|
private val ethereumUpstreamCreator = mock<EthereumUpstreamCreator>()
|
||||||
private val bitcoinUpstreamCreator = mock<BitcoinUpstreamCreator>()
|
private val bitcoinUpstreamCreator = mock<BitcoinUpstreamCreator>()
|
||||||
private val upstreamFactory = UpstreamFactory(genericUpstreamCreator, ethereumUpstreamCreator, bitcoinUpstreamCreator)
|
private val grpcUpstreamCreator = mock<GrpcUpstreamCreator>()
|
||||||
|
private val upstreamFactory = UpstreamFactory(
|
||||||
|
genericUpstreamCreator,
|
||||||
|
ethereumUpstreamCreator,
|
||||||
|
bitcoinUpstreamCreator,
|
||||||
|
grpcUpstreamCreator,
|
||||||
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun data() = listOf(
|
fun data() = listOf(
|
||||||
|
|||||||
Reference in New Issue
Block a user