solution: select which kind of upstream to use for native call

This commit is contained in:
Igor Artamonov
2019-08-02 23:46:58 -04:00
parent 2c5551d818
commit 22c2294983
19 changed files with 607 additions and 94 deletions

View File

@@ -94,6 +94,16 @@ class UpstreamsConfig {
//TODO make it unmodifiable after initial load
class Labels: HashMap<String, String>() {
companion object {
@JvmStatic fun fromMap(map: Map<String, String>): Labels {
val labels = Labels()
map.entries.forEach() { kv ->
labels.put(kv.key, kv.value)
}
return labels
}
}
}
enum class UpstreamType private constructor(vararg code: String) {

View File

@@ -5,9 +5,11 @@ import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.EthereumApi
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@@ -30,10 +32,11 @@ class NativeCall(
return requestMono.flatMapMany { request ->
val chain= Chain.byId(request.chain.number)
if (chain == Chain.UNSPECIFIED) {
// TODO send error to all requests?
throw Exception("Invalid chain id: ${request.chain.number}")
}
// TODO send error to all requests?
val upstream = upstreams.getUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
val matcher = Selector.convertToMatcher(request.selector)
val upstream = upstreams.getUpstream(chain)?.getApi(matcher) ?: throw Exception("Chain ${chain.id} is unavailable")
request.itemsList.toFlux().map {
val method = it.target
val params = it.payload.toStringUtf8()

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
@@ -68,7 +69,9 @@ class TrackAddress(
}
private fun stopTracking(client: TrackedAddress) {
clients[client.chain]?.remove(client) ?: log.warn("Chain ${client.chain} is not available for tracking")
clients[client.chain]?.removeIf {
it.id == client.id
} ?: log.warn("Chain ${client.chain} is not available for tracking")
}
fun isTracked(chain: Chain, address: Address): Boolean {
@@ -151,7 +154,7 @@ class TrackAddress(
fun getBalance(addr: SimpleAddress): Mono<Wei> {
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(Exception("Unsupported chain: ${addr.chain}"))
return up.getApi()
return up.getApi(Selector.empty)
.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST))
.timeout(Duration.ofSeconds(15))
}
@@ -208,9 +211,5 @@ class TrackAddress(
val id: Long
): SimpleAddress(chain, address, balance) {
override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance, id)
override fun equals(other: Any?): Boolean {
return other != null && other is TrackedAddress && other.id == id
}
}
}

View File

@@ -4,6 +4,7 @@ import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
@@ -99,7 +100,7 @@ class TrackTx(
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
val upstream = upstreams.getUpstream(tx.chain)
?: return Mono.error(Exception("Unsupported blockchain: ${tx.chain}"))
return upstream.getApi()
return upstream.getApi(Selector.empty)
.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash))
.map { block ->
if (block != null && block.number != null && block.totalDifficulty != null) {
@@ -119,7 +120,7 @@ class TrackTx(
private fun checkForUpdate(tx: TrackedTx): Mono<TrackedTx> {
val upstream = upstreams.getUpstream(tx.chain) ?: return Mono.error(Exception("Unsupported blockchain: ${tx.chain}"))
val execution = upstream.getApi()
val execution = upstream.getApi(Selector.empty)
.executeAndConvert(Commands.eth().getTransaction(tx.txid))
return execution.flatMap {
if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {

View File

@@ -11,7 +11,7 @@ abstract class AggregatedUpstreams: Upstream {
abstract fun getAll(): List<Upstream>
abstract fun addUpstream(upstream: Upstream)
abstract fun getApis(quorum: Int): Iterator<EthereumApi>
abstract fun getApis(quorum: Int, matcher: Selector.Matcher): Iterator<EthereumApi>
override fun observeStatus(): Flux<UpstreamAvailability> {
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
@@ -28,8 +28,8 @@ abstract class AggregatedUpstreams: Upstream {
return list
}
override fun isAvailable(): Boolean {
return getAll().any { it.isAvailable() }
override fun isAvailable(matcher: Selector.Matcher): Boolean {
return getAll().any { it.isAvailable(matcher) }
}
override fun getStatus(): UpstreamAvailability {
@@ -39,54 +39,7 @@ abstract class AggregatedUpstreams: Upstream {
}
override fun getOptions(): UpstreamsConfig.Options {
val options = UpstreamsConfig.Options()
options.quorum = getAll().filter {
it.getStatus() == UpstreamAvailability.OK
}.sumBy {
it.getOptions().quorum
}
return options
}
class SingleApi(
private val quorumApi: QuorumApi
): Iterator<EthereumApi> {
private var consumed = false
override fun hasNext(): Boolean {
return !consumed && quorumApi.hasNext()
}
override fun next(): EthereumApi {
consumed = true
return quorumApi.next()
}
}
class QuorumApi(
private val apis: List<Upstream>,
private val quorum: Int,
private var pos: Int
): Iterator<EthereumApi> {
private var consumed = 0
override fun hasNext(): Boolean {
return consumed < quorum
}
override fun next(): EthereumApi {
val start = pos
while (pos < start + apis.size) {
val api = apis[pos++ % apis.size]
if (api.isAvailable()) {
consumed++
return api.getApi()
}
}
throw IllegalStateException("No upstream API available")
}
return UpstreamsConfig.Options()
}
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())

View File

@@ -1,6 +1,5 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import java.io.Closeable
@@ -44,16 +43,16 @@ class ChainUpstreams (
head = updateHead()
}
override fun getApis(quorum: Int): Iterator<EthereumApi> {
override fun getApis(quorum: Int, matcher: Selector.Matcher): Iterator<EthereumApi> {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return QuorumApi(upstreams, 1, seq)
return FilteringApiIterator(upstreams, 1, seq, matcher)
}
override fun getApi(): EthereumApi {
return getApis(1).next()
override fun getApi(matcher: Selector.Matcher): EthereumApi {
return getApis(1, matcher).next()
}
override fun getHead(): EthereumHead {

View File

@@ -12,7 +12,6 @@ import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletableFuture
open class EthereumApi(
val rpcClient: RpcClient,

View File

@@ -21,14 +21,27 @@ import java.util.concurrent.CompletableFuture
import java.util.function.Function
class EthereumGrpcTransport(
private val chain: Chain,
private val chainRef: Common.ChainRef,
private val selector: BlockchainOuterClass.Selector?,
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper
): RpcTransport {
private val chainRef = Common.ChainRef.forNumber(chain.id)
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
constructor(
chain: Chain,
client: ReactorBlockchainGrpc.ReactorBlockchainStub,
objectMapper: ObjectMapper
) : this(Common.ChainRef.forNumber(chain.id), Selector.EmptyMatcher().asProto(), client, objectMapper)
fun withMatcher(matcher: Selector.Matcher): EthereumGrpcTransport {
if (matcher is Selector.EmptyMatcher && selector == null) {
return this
}
return EthereumGrpcTransport(chainRef, matcher.asProto(), client, objectMapper)
}
override fun close() {
}
@@ -87,7 +100,10 @@ class EthereumGrpcTransport(
override fun execute(items: List<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(chainRef);
.setChain(chainRef)
if (selector != null) {
req.setSelector(selector)
}
val mapping = prepareMapping(items, req)
return client.nativeCall(req.build())
.map(replyProcessor(mapping))

View File

@@ -9,7 +9,7 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import java.util.concurrent.atomic.AtomicReference
class EthereumUpstream(
open class EthereumUpstream(
val chain: Chain,
private val api: EthereumApi,
private val ethereumWs: EthereumWs? = null,
@@ -45,14 +45,18 @@ class EthereumUpstream(
}
}
override fun isAvailable(): Boolean {
return status.get() == UpstreamAvailability.OK
override fun isAvailable(matcher: Selector.Matcher): Boolean {
return status.get() == UpstreamAvailability.OK && matcher.matches(node.labels)
}
override fun getStatus(): UpstreamAvailability {
return status.get()
}
fun setStatus(avail: UpstreamAvailability) {
status.set(avail)
}
override fun observeStatus(): Flux<UpstreamAvailability> {
return Flux.from(statusStream)
}
@@ -61,7 +65,11 @@ class EthereumUpstream(
return head
}
override fun getApi(): EthereumApi {
override fun getApi(matcher: Selector.Matcher): EthereumApi {
return api
}
fun getApi(): EthereumApi {
return api
}

View File

@@ -0,0 +1,27 @@
package io.emeraldpay.dshackle.upstream
class FilteringApiIterator(
private val apis: List<Upstream>,
private val quorum: Int,
private var pos: Int,
private val matcher: Selector.Matcher
): Iterator<EthereumApi> {
private var consumed = 0
override fun hasNext(): Boolean {
return consumed < quorum
}
override fun next(): EthereumApi {
val start = pos
while (pos < start + apis.size) {
val api = apis[pos++ % apis.size]
if (api.isAvailable(matcher)) {
consumed++
return api.getApi(matcher)
}
}
throw IllegalStateException("No upstream API available")
}
}

View File

@@ -41,14 +41,13 @@ open class GrpcUpstream(
private val status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
private val head = Head(this)
private val api: EthereumApi
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
private val supportedMethods = HashSet<String>()
private val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
init {
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
val rpcClient = DefaultRpcClient(grpcTransport)
api = EthereumApi(rpcClient, objectMapper, chain)
open fun createApi(matcher: Selector.Matcher): EthereumApi {
val rpcClient = DefaultRpcClient(grpcTransport.withMatcher(matcher))
return EthereumApi(rpcClient, objectMapper, chain)
}
open fun connect() {
@@ -80,7 +79,7 @@ open class GrpcUpstream(
curr == null || curr.totalDifficulty < block.totalDifficulty
}
.flatMap {
getApi()
getApi(Selector.EmptyMatcher())
.executeAndConvert(Commands.eth().getBlock(it.hash))
.timeout(Duration.ofSeconds(15))
}
@@ -135,8 +134,10 @@ open class GrpcUpstream(
return supportedMethods
}
override fun isAvailable(): Boolean {
return headBlock.get() != null
override fun isAvailable(matcher: Selector.Matcher): Boolean {
return headBlock.get() != null && nodes.get().getNodes().any {
it.quorum > 0 && matcher.matches(it.labels)
}
}
override fun getStatus(): UpstreamAvailability {
@@ -151,8 +152,8 @@ open class GrpcUpstream(
return head
}
override fun getApi(): EthereumApi {
return api
override fun getApi(matcher: Selector.Matcher): EthereumApi {
return createApi(matcher)
}
override fun getOptions(): UpstreamsConfig.Options {

View File

@@ -0,0 +1,128 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.config.UpstreamsConfig
import org.apache.commons.lang3.StringUtils
import java.util.*
class Selector {
companion object {
val empty = EmptyMatcher()
@JvmStatic
fun convertToMatcher(req: BlockchainOuterClass.Selector?): Matcher {
return when {
req == null -> EmptyMatcher()
req.hasLabelSelector() -> req.labelSelector.let { selector ->
if (StringUtils.isNotEmpty(selector.name)) {
val values = selector.valueList
.map { it?.trim() ?: "" }
.filter { StringUtils.isNotEmpty(it) }
if (values.isEmpty()) {
ExistsMatcher(selector.name)
} else {
LabelMatcher(selector.name, selector.valueList)
}
} else {
EmptyMatcher()
}
}
req.hasAndSelector() -> AndMatcher(Collections.unmodifiableCollection(req.andSelector.selectorsList.map { convertToMatcher(it) }))
req.hasOrSelector() -> OrMatcher(Collections.unmodifiableCollection(req.orSelector.selectorsList.map { convertToMatcher(it) }))
req.hasNotSelector() -> NotMatcher(convertToMatcher(req.notSelector.selector))
req.hasExistsSelector() -> ExistsMatcher(req.existsSelector.name)
else -> EmptyMatcher()
}
}
}
interface Matcher {
fun matches(labels: UpstreamsConfig.Labels): Boolean
fun asProto(): BlockchainOuterClass.Selector?
}
class EmptyMatcher: Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return true
}
override fun asProto(): BlockchainOuterClass.Selector? {
return null
}
}
class LabelMatcher(val name: String, val values: Collection<String>): Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return labels.get(name)?.let {
labelValue -> values.any { it == labelValue }
} ?: false
}
override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setLabelSelector(
BlockchainOuterClass.LabelSelector.newBuilder()
.setName(name)
.addAllValue(values)
).build()
}
}
class OrMatcher(val matchers: Collection<Matcher>): Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return matchers.any { matcher -> matcher.matches(labels) }
}
override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setOrSelector(
BlockchainOuterClass.OrSelector.newBuilder()
.addAllSelectors(matchers.map { it.asProto() })
.build()
).build()
}
}
class AndMatcher(val matchers: Collection<Matcher>): Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return matchers.all { matcher -> matcher.matches(labels) }
}
override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setAndSelector(
BlockchainOuterClass.AndSelector.newBuilder()
.addAllSelectors(matchers.map { it.asProto() })
.build()
).build()
}
}
class NotMatcher(val matcher: Matcher): Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return !matcher.matches(labels)
}
override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setNotSelector(
BlockchainOuterClass.NotSelector.newBuilder()
.setSelector(matcher.asProto())
.build()
).build()
}
}
class ExistsMatcher(val name: String): Matcher {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return labels.containsKey(name)
}
override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setExistsSelector(
BlockchainOuterClass.ExistsSelector.newBuilder()
.setName(name)
.build()
).build()
}
}
}

View File

@@ -4,11 +4,11 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import reactor.core.publisher.Flux
interface Upstream {
fun isAvailable(): Boolean
fun isAvailable(matcher: Selector.Matcher): Boolean
fun getStatus(): UpstreamAvailability
fun observeStatus(): Flux<UpstreamAvailability>
fun getHead(): EthereumHead
fun getApi(): EthereumApi
fun getApi(matcher: Selector.Matcher): EthereumApi
fun getOptions(): UpstreamsConfig.Options
fun getSupportedTargets(): Set<String>
}

View File

@@ -17,7 +17,7 @@ class UpstreamValidator(
val peerCount = batch.add(Commands.net().peerCount())
val syncing = batch.add(Commands.eth().syncing())
try {
ethereumUpstream.getApi().rpcClient.execute(batch).get(5, TimeUnit.SECONDS)
ethereumUpstream.getApi(Selector.empty).rpcClient.execute(batch).get(5, TimeUnit.SECONDS)
if (syncing.get().isSyncing) {
return UpstreamAvailability.SYNCING
}