solution: select which kind of upstream to use for native call
This commit is contained in:
@@ -94,6 +94,16 @@ class UpstreamsConfig {
|
|||||||
|
|
||||||
//TODO make it unmodifiable after initial load
|
//TODO make it unmodifiable after initial load
|
||||||
class Labels: HashMap<String, String>() {
|
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) {
|
enum class UpstreamType private constructor(vararg code: String) {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import com.google.protobuf.ByteString
|
|||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.EthereumApi
|
import io.emeraldpay.dshackle.upstream.EthereumApi
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -30,10 +32,11 @@ class NativeCall(
|
|||||||
return requestMono.flatMapMany { request ->
|
return requestMono.flatMapMany { request ->
|
||||||
val chain= Chain.byId(request.chain.number)
|
val chain= Chain.byId(request.chain.number)
|
||||||
if (chain == Chain.UNSPECIFIED) {
|
if (chain == Chain.UNSPECIFIED) {
|
||||||
|
// TODO send error to all requests?
|
||||||
throw Exception("Invalid chain id: ${request.chain.number}")
|
throw Exception("Invalid chain id: ${request.chain.number}")
|
||||||
}
|
}
|
||||||
// TODO send error to all requests?
|
val matcher = Selector.convertToMatcher(request.selector)
|
||||||
val upstream = upstreams.getUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
|
val upstream = upstreams.getUpstream(chain)?.getApi(matcher) ?: throw Exception("Chain ${chain.id} is unavailable")
|
||||||
request.itemsList.toFlux().map {
|
request.itemsList.toFlux().map {
|
||||||
val method = it.target
|
val method = it.target
|
||||||
val params = it.payload.toStringUtf8()
|
val params = it.payload.toStringUtf8()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
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.upstream.AvailableChains
|
import io.emeraldpay.dshackle.upstream.AvailableChains
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.Address
|
import io.infinitape.etherjar.domain.Address
|
||||||
@@ -68,7 +69,9 @@ class TrackAddress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopTracking(client: TrackedAddress) {
|
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 {
|
fun isTracked(chain: Chain, address: Address): Boolean {
|
||||||
@@ -151,7 +154,7 @@ class TrackAddress(
|
|||||||
|
|
||||||
fun getBalance(addr: SimpleAddress): Mono<Wei> {
|
fun getBalance(addr: SimpleAddress): Mono<Wei> {
|
||||||
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(Exception("Unsupported chain: ${addr.chain}"))
|
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))
|
.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST))
|
||||||
.timeout(Duration.ofSeconds(15))
|
.timeout(Duration.ofSeconds(15))
|
||||||
}
|
}
|
||||||
@@ -208,9 +211,5 @@ class TrackAddress(
|
|||||||
val id: Long
|
val id: Long
|
||||||
): SimpleAddress(chain, address, balance) {
|
): SimpleAddress(chain, address, balance) {
|
||||||
override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance, id)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.google.protobuf.ByteString
|
|||||||
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.upstream.AvailableChains
|
import io.emeraldpay.dshackle.upstream.AvailableChains
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
@@ -99,7 +100,7 @@ class TrackTx(
|
|||||||
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
|
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
|
||||||
val upstream = upstreams.getUpstream(tx.chain)
|
val upstream = upstreams.getUpstream(tx.chain)
|
||||||
?: return Mono.error(Exception("Unsupported blockchain: ${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))
|
.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash))
|
||||||
.map { block ->
|
.map { block ->
|
||||||
if (block != null && block.number != null && block.totalDifficulty != null) {
|
if (block != null && block.number != null && block.totalDifficulty != null) {
|
||||||
@@ -119,7 +120,7 @@ class TrackTx(
|
|||||||
|
|
||||||
private fun checkForUpdate(tx: TrackedTx): Mono<TrackedTx> {
|
private fun checkForUpdate(tx: TrackedTx): Mono<TrackedTx> {
|
||||||
val upstream = upstreams.getUpstream(tx.chain) ?: return Mono.error(Exception("Unsupported blockchain: ${tx.chain}"))
|
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))
|
.executeAndConvert(Commands.eth().getTransaction(tx.txid))
|
||||||
return execution.flatMap {
|
return execution.flatMap {
|
||||||
if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ abstract class AggregatedUpstreams: Upstream {
|
|||||||
|
|
||||||
abstract fun getAll(): List<Upstream>
|
abstract fun getAll(): List<Upstream>
|
||||||
abstract fun addUpstream(upstream: 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> {
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
||||||
@@ -28,8 +28,8 @@ abstract class AggregatedUpstreams: Upstream {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
override fun isAvailable(matcher: Selector.Matcher): Boolean {
|
||||||
return getAll().any { it.isAvailable() }
|
return getAll().any { it.isAvailable(matcher) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(): UpstreamAvailability {
|
override fun getStatus(): UpstreamAvailability {
|
||||||
@@ -39,54 +39,7 @@ abstract class AggregatedUpstreams: Upstream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getOptions(): UpstreamsConfig.Options {
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
val options = UpstreamsConfig.Options()
|
return 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
@@ -44,16 +43,16 @@ class ChainUpstreams (
|
|||||||
head = updateHead()
|
head = updateHead()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApis(quorum: Int): Iterator<EthereumApi> {
|
override fun getApis(quorum: Int, matcher: Selector.Matcher): Iterator<EthereumApi> {
|
||||||
val i = seq++
|
val i = seq++
|
||||||
if (seq >= Int.MAX_VALUE / 2) {
|
if (seq >= Int.MAX_VALUE / 2) {
|
||||||
seq = 0
|
seq = 0
|
||||||
}
|
}
|
||||||
return QuorumApi(upstreams, 1, seq)
|
return FilteringApiIterator(upstreams, 1, seq, matcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(): EthereumApi {
|
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||||
return getApis(1).next()
|
return getApis(1, matcher).next()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHead(): EthereumHead {
|
override fun getHead(): EthereumHead {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import reactor.core.publisher.Mono
|
|||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
open class EthereumApi(
|
open class EthereumApi(
|
||||||
val rpcClient: RpcClient,
|
val rpcClient: RpcClient,
|
||||||
|
|||||||
@@ -21,14 +21,27 @@ import java.util.concurrent.CompletableFuture
|
|||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
class EthereumGrpcTransport(
|
class EthereumGrpcTransport(
|
||||||
private val chain: Chain,
|
private val chainRef: Common.ChainRef,
|
||||||
|
private val selector: BlockchainOuterClass.Selector?,
|
||||||
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
): RpcTransport {
|
): RpcTransport {
|
||||||
|
|
||||||
private val chainRef = Common.ChainRef.forNumber(chain.id)
|
|
||||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
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() {
|
override fun close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +100,10 @@ class EthereumGrpcTransport(
|
|||||||
|
|
||||||
override fun execute(items: List<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
|
override fun execute(items: List<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
|
||||||
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChain(chainRef);
|
.setChain(chainRef)
|
||||||
|
if (selector != null) {
|
||||||
|
req.setSelector(selector)
|
||||||
|
}
|
||||||
val mapping = prepareMapping(items, req)
|
val mapping = prepareMapping(items, req)
|
||||||
return client.nativeCall(req.build())
|
return client.nativeCall(req.build())
|
||||||
.map(replyProcessor(mapping))
|
.map(replyProcessor(mapping))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.TopicProcessor
|
import reactor.core.publisher.TopicProcessor
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class EthereumUpstream(
|
open class EthereumUpstream(
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
private val api: EthereumApi,
|
private val api: EthereumApi,
|
||||||
private val ethereumWs: EthereumWs? = null,
|
private val ethereumWs: EthereumWs? = null,
|
||||||
@@ -45,14 +45,18 @@ class EthereumUpstream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
override fun isAvailable(matcher: Selector.Matcher): Boolean {
|
||||||
return status.get() == UpstreamAvailability.OK
|
return status.get() == UpstreamAvailability.OK && matcher.matches(node.labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(): UpstreamAvailability {
|
override fun getStatus(): UpstreamAvailability {
|
||||||
return status.get()
|
return status.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setStatus(avail: UpstreamAvailability) {
|
||||||
|
status.set(avail)
|
||||||
|
}
|
||||||
|
|
||||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
return Flux.from(statusStream)
|
return Flux.from(statusStream)
|
||||||
}
|
}
|
||||||
@@ -61,7 +65,11 @@ class EthereumUpstream(
|
|||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(): EthereumApi {
|
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApi(): EthereumApi {
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,14 +41,13 @@ open class GrpcUpstream(
|
|||||||
private val status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
|
private val status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
|
||||||
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
|
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
|
||||||
private val head = Head(this)
|
private val head = Head(this)
|
||||||
private val api: EthereumApi
|
|
||||||
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||||
private val supportedMethods = HashSet<String>()
|
private val supportedMethods = HashSet<String>()
|
||||||
|
private val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
|
||||||
|
|
||||||
init {
|
open fun createApi(matcher: Selector.Matcher): EthereumApi {
|
||||||
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
|
val rpcClient = DefaultRpcClient(grpcTransport.withMatcher(matcher))
|
||||||
val rpcClient = DefaultRpcClient(grpcTransport)
|
return EthereumApi(rpcClient, objectMapper, chain)
|
||||||
api = EthereumApi(rpcClient, objectMapper, chain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun connect() {
|
open fun connect() {
|
||||||
@@ -80,7 +79,7 @@ open class GrpcUpstream(
|
|||||||
curr == null || curr.totalDifficulty < block.totalDifficulty
|
curr == null || curr.totalDifficulty < block.totalDifficulty
|
||||||
}
|
}
|
||||||
.flatMap {
|
.flatMap {
|
||||||
getApi()
|
getApi(Selector.EmptyMatcher())
|
||||||
.executeAndConvert(Commands.eth().getBlock(it.hash))
|
.executeAndConvert(Commands.eth().getBlock(it.hash))
|
||||||
.timeout(Duration.ofSeconds(15))
|
.timeout(Duration.ofSeconds(15))
|
||||||
}
|
}
|
||||||
@@ -135,8 +134,10 @@ open class GrpcUpstream(
|
|||||||
return supportedMethods
|
return supportedMethods
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
override fun isAvailable(matcher: Selector.Matcher): Boolean {
|
||||||
return headBlock.get() != null
|
return headBlock.get() != null && nodes.get().getNodes().any {
|
||||||
|
it.quorum > 0 && matcher.matches(it.labels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(): UpstreamAvailability {
|
override fun getStatus(): UpstreamAvailability {
|
||||||
@@ -151,8 +152,8 @@ open class GrpcUpstream(
|
|||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(): EthereumApi {
|
override fun getApi(matcher: Selector.Matcher): EthereumApi {
|
||||||
return api
|
return createApi(matcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOptions(): UpstreamsConfig.Options {
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
|
|||||||
128
src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt
Normal file
128
src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
interface Upstream {
|
interface Upstream {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(matcher: Selector.Matcher): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
fun observeStatus(): Flux<UpstreamAvailability>
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
fun getHead(): EthereumHead
|
fun getHead(): EthereumHead
|
||||||
fun getApi(): EthereumApi
|
fun getApi(matcher: Selector.Matcher): EthereumApi
|
||||||
fun getOptions(): UpstreamsConfig.Options
|
fun getOptions(): UpstreamsConfig.Options
|
||||||
fun getSupportedTargets(): Set<String>
|
fun getSupportedTargets(): Set<String>
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ class UpstreamValidator(
|
|||||||
val peerCount = batch.add(Commands.net().peerCount())
|
val peerCount = batch.add(Commands.net().peerCount())
|
||||||
val syncing = batch.add(Commands.eth().syncing())
|
val syncing = batch.add(Commands.eth().syncing())
|
||||||
try {
|
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) {
|
if (syncing.get().isSyncing) {
|
||||||
return UpstreamAvailability.SYNCING
|
return UpstreamAvailability.SYNCING
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.time.Duration
|
|||||||
|
|
||||||
class TrackAddressSpec extends Specification {
|
class TrackAddressSpec extends Specification {
|
||||||
|
|
||||||
AvailableChains availableChains = new AvailableChains()
|
AvailableChains availableChains
|
||||||
Upstreams upstreams
|
Upstreams upstreams
|
||||||
TrackAddress trackAddress
|
TrackAddress trackAddress
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
|
|
||||||
|
|
||||||
def setup() {
|
def setup() {
|
||||||
|
availableChains = new AvailableChains()
|
||||||
upstreams = Mock(Upstreams)
|
upstreams = Mock(Upstreams)
|
||||||
trackAddress = new TrackAddress(upstreams, availableChains, Schedulers.immediate())
|
trackAddress = new TrackAddress(upstreams, availableChains, Schedulers.immediate())
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
def apiMock = new EthereumApiMock(Mock(RpcClient), TestingCommons.objectMapper(), Chain.ETHEREUM)
|
def apiMock = new EthereumApiMock(Mock(RpcClient), TestingCommons.objectMapper(), Chain.ETHEREUM)
|
||||||
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
||||||
_ * upstreamMock.getApi() >> apiMock
|
_ * upstreamMock.getApi(_) >> apiMock
|
||||||
start()
|
start()
|
||||||
when:
|
when:
|
||||||
def flux = trackAddress.getBalance(Mono.just(req))
|
def flux = trackAddress.getBalance(Mono.just(req))
|
||||||
@@ -106,7 +107,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98")
|
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98")
|
||||||
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
||||||
_ * upstreamMock.getApi() >> apiMock
|
_ * upstreamMock.getApi(_) >> apiMock
|
||||||
_ * upstreamMock.getHead() >> headMock
|
_ * upstreamMock.getHead() >> headMock
|
||||||
_ * headMock.getFlux() >> blocksBus
|
_ * headMock.getFlux() >> blocksBus
|
||||||
start()
|
start()
|
||||||
@@ -124,6 +125,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
.expectNext(exp2)
|
.expectNext(exp2)
|
||||||
.thenCancel()
|
.thenCancel()
|
||||||
.verify(Duration.ofSeconds(3))
|
.verify(Duration.ofSeconds(3))
|
||||||
|
Thread.sleep(50)
|
||||||
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
|
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class TrackTxSpec extends Specification {
|
|||||||
apiMock.answer("eth_getBlockByHash", [blockJson.hash.toHex(), false], blockJson)
|
apiMock.answer("eth_getBlockByHash", [blockJson.hash.toHex(), false], blockJson)
|
||||||
|
|
||||||
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
||||||
_ * upstreamMock.getApi() >> apiMock
|
_ * upstreamMock.getApi(_) >> apiMock
|
||||||
_ * upstreamMock.getHead() >> headMock
|
_ * upstreamMock.getHead() >> headMock
|
||||||
_ * headMock.getFlux() >> blocksBus
|
_ * headMock.getFlux() >> blocksBus
|
||||||
_ * headMock.getHead() >> Mono.just(blockHeadJson)
|
_ * headMock.getHead() >> Mono.just(blockHeadJson)
|
||||||
@@ -182,7 +182,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def headBlock = blocks[0]
|
def headBlock = blocks[0]
|
||||||
|
|
||||||
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
|
||||||
_ * upstreamMock.getApi() >> apiMock
|
_ * upstreamMock.getApi(_) >> apiMock
|
||||||
_ * upstreamMock.getHead() >> headMock
|
_ * upstreamMock.getHead() >> headMock
|
||||||
_ * headMock.getFlux() >> blocksBus
|
_ * headMock.getFlux() >> blocksBus
|
||||||
_ * headMock.getHead() >> { return Mono.just(headBlock) }
|
_ * headMock.getHead() >> { return Mono.just(headBlock) }
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class EthereumGrpcTransportSpec extends Specification {
|
|||||||
|
|
||||||
then:
|
then:
|
||||||
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
1 * otherSideAggr.api >> otherSideApi
|
1 * otherSideAggr.getApi(_) >> otherSideApi
|
||||||
status.failed == 0
|
status.failed == 0
|
||||||
status.succeed == 1
|
status.succeed == 1
|
||||||
status.total == 1
|
status.total == 1
|
||||||
@@ -90,7 +90,7 @@ class EthereumGrpcTransportSpec extends Specification {
|
|||||||
|
|
||||||
then:
|
then:
|
||||||
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
1 * otherSideAggr.api >> otherSideApi
|
1 * otherSideAggr.getApi(_) >> otherSideApi
|
||||||
status.failed == 0
|
status.failed == 0
|
||||||
status.succeed == 2
|
status.succeed == 2
|
||||||
status.total == 2
|
status.total == 2
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.DefaultRpcClient
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class FilteringApiIteratorSpec extends Specification {
|
||||||
|
|
||||||
|
def rpcClient = new DefaultRpcClient(null)
|
||||||
|
def objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
|
def "Verifies labels"() {
|
||||||
|
setup:
|
||||||
|
def upstreams = [
|
||||||
|
new EthereumUpstream(
|
||||||
|
Chain.ETHEREUM,
|
||||||
|
new EthereumApi(rpcClient, objectMapper, Chain.ETHEREUM),
|
||||||
|
(EthereumWs)null,
|
||||||
|
new UpstreamsConfig.Options(),
|
||||||
|
new NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels.fromMap([test: "foo"]))
|
||||||
|
),
|
||||||
|
new EthereumUpstream(
|
||||||
|
Chain.ETHEREUM,
|
||||||
|
new EthereumApi(rpcClient, objectMapper, Chain.ETHEREUM),
|
||||||
|
(EthereumWs)null,
|
||||||
|
new UpstreamsConfig.Options(),
|
||||||
|
new NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels.fromMap([test: "bar"]))
|
||||||
|
),
|
||||||
|
new EthereumUpstream(
|
||||||
|
Chain.ETHEREUM,
|
||||||
|
new EthereumApi(rpcClient, objectMapper, Chain.ETHEREUM),
|
||||||
|
(EthereumWs)null,
|
||||||
|
new UpstreamsConfig.Options(),
|
||||||
|
new NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels.fromMap([test: "foo", test2: "baz"]))
|
||||||
|
),
|
||||||
|
new EthereumUpstream(
|
||||||
|
Chain.ETHEREUM,
|
||||||
|
new EthereumApi(rpcClient, objectMapper, Chain.ETHEREUM),
|
||||||
|
(EthereumWs)null,
|
||||||
|
new UpstreamsConfig.Options(),
|
||||||
|
new NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels.fromMap([test: "foo"]))
|
||||||
|
),
|
||||||
|
new EthereumUpstream(
|
||||||
|
Chain.ETHEREUM,
|
||||||
|
new EthereumApi(rpcClient, objectMapper, Chain.ETHEREUM),
|
||||||
|
(EthereumWs)null,
|
||||||
|
new UpstreamsConfig.Options(),
|
||||||
|
new NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels.fromMap([test: "baz"]))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
def matcher = new Selector.LabelMatcher("test", ["foo"])
|
||||||
|
upstreams.forEach {
|
||||||
|
it.setStatus(UpstreamAvailability.OK)
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def iter = new FilteringApiIterator(upstreams, 3, 0, matcher)
|
||||||
|
then:
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[0].api
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[2].api
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[3].api
|
||||||
|
!iter.hasNext()
|
||||||
|
|
||||||
|
when:
|
||||||
|
iter = new FilteringApiIterator(upstreams, 2, 1, matcher)
|
||||||
|
then:
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[2].api
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[3].api
|
||||||
|
!iter.hasNext()
|
||||||
|
|
||||||
|
when:
|
||||||
|
iter = new FilteringApiIterator(upstreams, 3, 1, matcher)
|
||||||
|
then:
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[2].api
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[3].api
|
||||||
|
iter.hasNext()
|
||||||
|
iter.next() == upstreams[0].api
|
||||||
|
!iter.hasNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class SelectorSpec extends Specification {
|
||||||
|
|
||||||
|
private BlockchainOuterClass.LabelSelector.Builder selectLabel1 = BlockchainOuterClass.LabelSelector.newBuilder()
|
||||||
|
.setName("foo").addAllValue(["bar"])
|
||||||
|
private BlockchainOuterClass.Selector selectLabel1Selector = BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setLabelSelector(selectLabel1).build()
|
||||||
|
|
||||||
|
private BlockchainOuterClass.LabelSelector.Builder selectLabel2 = BlockchainOuterClass.LabelSelector.newBuilder()
|
||||||
|
.setName("baz").addAllValue(["bar"])
|
||||||
|
private BlockchainOuterClass.Selector selectLabel2Selector = BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setLabelSelector(selectLabel2).build()
|
||||||
|
|
||||||
|
def "Convert nothing"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(null)
|
||||||
|
then:
|
||||||
|
act.class == Selector.EmptyMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert LABEL match"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
selectLabel1Selector
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.LabelMatcher
|
||||||
|
with((Selector.LabelMatcher)act) {
|
||||||
|
name == "foo"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert EXISTS match"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setExistsSelector(
|
||||||
|
BlockchainOuterClass.ExistsSelector.newBuilder()
|
||||||
|
.setName("foo")
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.ExistsMatcher
|
||||||
|
with((Selector.ExistsMatcher)act) {
|
||||||
|
name == "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert NOT match"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setNotSelector(
|
||||||
|
BlockchainOuterClass.NotSelector.newBuilder()
|
||||||
|
.setSelector(selectLabel1Selector).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.NotMatcher
|
||||||
|
with((Selector.NotMatcher)act) {
|
||||||
|
matcher instanceof Selector.LabelMatcher
|
||||||
|
with((Selector.LabelMatcher)matcher) {
|
||||||
|
name == "foo"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert OR match"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setOrSelector(
|
||||||
|
BlockchainOuterClass.OrSelector.newBuilder()
|
||||||
|
.addAllSelectors([selectLabel1Selector, selectLabel2Selector]).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.OrMatcher
|
||||||
|
with((Selector.OrMatcher)act) {
|
||||||
|
matchers.size() == 2
|
||||||
|
with((Selector.LabelMatcher)matchers[0]) {
|
||||||
|
name == "foo"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
with((Selector.LabelMatcher)matchers[1]) {
|
||||||
|
name == "baz"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert AND match"() {
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setAndSelector(
|
||||||
|
BlockchainOuterClass.AndSelector.newBuilder()
|
||||||
|
.addAllSelectors([selectLabel1Selector, selectLabel2Selector]).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.AndMatcher
|
||||||
|
with((Selector.AndMatcher)act) {
|
||||||
|
matchers.size() == 2
|
||||||
|
with((Selector.LabelMatcher)matchers[0]) {
|
||||||
|
name == "foo"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
with((Selector.LabelMatcher)matchers[1]) {
|
||||||
|
name == "baz"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Convert LABEL AND NOT LABEl match"() {
|
||||||
|
setup:
|
||||||
|
def label1 = selectLabel1Selector
|
||||||
|
def label2 = selectLabel2Selector
|
||||||
|
def notLabel2 = BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setNotSelector(BlockchainOuterClass.NotSelector.newBuilder().setSelector(label2).build())
|
||||||
|
.build()
|
||||||
|
def and = BlockchainOuterClass.AndSelector.newBuilder()
|
||||||
|
.addAllSelectors([
|
||||||
|
label1, notLabel2
|
||||||
|
]).build()
|
||||||
|
when:
|
||||||
|
def act = Selector.convertToMatcher(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setAndSelector(and)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
then:
|
||||||
|
act instanceof Selector.AndMatcher
|
||||||
|
with((Selector.AndMatcher)act) {
|
||||||
|
matchers.size() == 2
|
||||||
|
with((Selector.LabelMatcher)matchers[0]) {
|
||||||
|
name == "foo"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
matchers[1] instanceof Selector.NotMatcher
|
||||||
|
with((Selector.NotMatcher)matchers[1]) {
|
||||||
|
matcher instanceof Selector.LabelMatcher
|
||||||
|
with((Selector.LabelMatcher)matcher) {
|
||||||
|
name == "baz"
|
||||||
|
values == ["bar"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "LABEL matches single label"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.LabelMatcher("test", ["foo"])
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo"],
|
||||||
|
[test: "foo", test2: "bar"],
|
||||||
|
[test2: "foo", test: "foo"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "LABEL matches one label two values"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.LabelMatcher("test", ["foo", "bar"])
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo"],
|
||||||
|
[test: "foo", test2: "bar"],
|
||||||
|
[test2: "foo", test: "bar"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "AND matches one label"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.AndMatcher(
|
||||||
|
[
|
||||||
|
new Selector.LabelMatcher("test", ["foo", "bar"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo"],
|
||||||
|
[test: "foo", test2: "bar"],
|
||||||
|
[test2: "foo", test: "bar"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "AND matches two labels"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.AndMatcher(
|
||||||
|
[
|
||||||
|
new Selector.LabelMatcher("test", ["foo", "bar"]),
|
||||||
|
new Selector.LabelMatcher("test2", ["baz"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo", test2: "baz"],
|
||||||
|
[test: "foo", test3: "bar", test2: "baz"],
|
||||||
|
[test3: "foo", test: "bar", test2: "baz"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "OR matches two labels"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.OrMatcher(
|
||||||
|
[
|
||||||
|
new Selector.LabelMatcher("test", ["foo", "bar"]),
|
||||||
|
new Selector.LabelMatcher("test2", ["baz"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo", test2: "baz"],
|
||||||
|
[test: "foo", test3: "bar", test2: "baz"],
|
||||||
|
[test3: "foo", test: "bar", test2: "baz"],
|
||||||
|
[test2: "baz"],
|
||||||
|
[test: "bar"],
|
||||||
|
[test: "foo"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "AND NOT matches two labels"() {
|
||||||
|
setup:
|
||||||
|
def matcher = new Selector.AndMatcher(
|
||||||
|
[
|
||||||
|
new Selector.LabelMatcher("test", ["foo", "bar"]),
|
||||||
|
new Selector.NotMatcher(
|
||||||
|
new Selector.LabelMatcher("test2", ["baz"])
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect:
|
||||||
|
matcher.matches(UpstreamsConfig.Labels.fromMap(maps))
|
||||||
|
|
||||||
|
where:
|
||||||
|
maps << [
|
||||||
|
[test: "foo", test2: "not_baz"],
|
||||||
|
[test: "foo", test3: "bar", test2: "not_baz"],
|
||||||
|
[test: "foo", test3: "bar"],
|
||||||
|
[test3: "foo", test: "bar", test2: "not_baz"],
|
||||||
|
[test3: "foo", test: "bar"],
|
||||||
|
[test: "bar"],
|
||||||
|
[test: "foo"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user